Rate limits

Rate limits

The RollerUp API limits requests by query cost, not by request count. Each operation is assigned a cost in points based on what it asks for. That cost is taken from a budget that refills continuously. A small query uses very little budget, and a large paginated query uses more.

Rate limits apply to requests made with an API token (tokens that start with rup_).

How cost is calculated

The server calculates the cost of your operation before running it:

Part of the query Cost
A field that returns an object (has its own { … } selection) 1 point
A field that returns a scalar or enum (id, plate, createdAt, …) 0 points
Each top-level field of a mutation 10 points
A paginated field (first or last) 1 point, plus its selection multiplied by the page size

Pagination counts where you pass it: directly (cars(first: 50)), inside an input object (cars(input: { first: 50 })), or in a variable (cars(input: $input)). Introspection (__schema, __type, __typename) is free.

A few fields that do much more work on the server than their shape suggests have a fixed cost instead. The schema reference shows this as Rate-limit cost on the field.

Examples

A single record with scalar fields costs 1 point: car is the only object.

query {
  car(id: "abc123") {
    id
    plate
    make
  }
}

A page of 50 records costs 151 points. cars costs 1. Its selection (nodes, customer and pageInfo, 1 point each) costs 3, multiplied by 50.

query {
  cars(input: { first: 50 }) { # 1, plus everything inside × 50
    nodes {                    #   1
      id                       #   0
      plate                    #   0
      customer {               #   1
        id                     #   0
        email                  #   0
      }
    }
    pageInfo {                 #   1
      hasNextPage              #   0
      endCursor                #   0
    }
  }                            # inside: 3 × 50 = 150
}                              # total: 1 + 150 = 151

A mutation that returns an object costs 11 points: 10 for the mutation and 1 for car.

mutation AddCar($input: AddCarToMembershipInput!) {
  addCarToMembership(input: $input) {
    id
    car {
      id
    }
  }
}

Your budget

Budgets are token buckets. Each request takes its cost out of the bucket, and the bucket refills at a steady rate up to its capacity. A client that sends requests faster than the bucket refills uses up its capacity and is then throttled. A client that stays under the refill rate never is:

Points left in a user bucket over 30 secondsA client sending a 151-point request every half second empties the 1,000-point bucket in about 3.5 seconds, is throttled 12 times, and is then held to one request about every 3 seconds. A client sending one every 4 seconds never drops below 849 points and is never throttled.025050075010000s5s10s15s20s25s30spoints left
One request every 4 seconds: never throttled. One every half second: empties the bucket in about 3.5 seconds. Each × is a throttled request, after which it waits retryAfterMs. Default user bucket, 151-point requests.

There are two buckets, and a request has to fit in both. These are the default sizes:

Bucket Default capacity Default refill rate
Company, shared by everyone at your company 2,000 points 100 points per second
User, per user, shared by all of that user’s API tokens 1,000 points 50 points per second

Some companies have higher limits. Every response reports your actual limits in extensions.cost, so read them from there and don’t hard-code the numbers above. If your integration needs a higher limit, contact your RollerUp representative.

The user bucket is half of the company bucket, so one integration can’t use up the budget for everyone else at your company. When the company limit is raised, the user limit rises with it.

The company budget is shared

When the company bucket runs out, every API token at your company is throttled, including users who still have budget of their own, until it refills. One user can only use half of it, but two busy integrations together can use all of it. Coordinate heavy jobs across your integrations, and handle scope: "COMPANY" throttling as described under Errors.

A single operation can cost at most 1,000 points, or your user bucket’s capacity if that’s smaller, however much budget is left. A query over that limit is rejected outright (see Errors). Split it into smaller pages instead.

Putting it together, every API-token request goes through these checks:

flowchart LR A("API request") --> B("Calculate<br/>the query's cost") B --> C("Over the<br/>single-query limit?") C -- No --> D("Fits in your user<br/>and company buckets?") D -- Yes --> G("Request runs<br/>cost taken from both buckets") C -- Yes --> E("QUERY_TOO_EXPENSIVE<br/>HTTP 400 · don't retry") D -- No --> F("THROTTLED<br/>HTTP 429 · nothing charged<br/>retry after retryAfterMs") class C,D decision class E bad class F warn class G ok

Reading your remaining budget

Every metered response includes the cost of the request and the state of your budget in extensions.cost:

{
  "data": { "...": "..." },
  "extensions": {
    "cost": {
      "requestedQueryCost": 151,
      "throttleStatus": {
        "scope": "USER",
        "maximumAvailable": 1000,
        "currentlyAvailable": 849,
        "restoreRate": 50,
        "retryAfterMs": 0,
        "buckets": {
          "user": { "maximumAvailable": 1000, "currentlyAvailable": 849, "restoreRate": 50 },
          "company": { "maximumAvailable": 2000, "currentlyAvailable": 1849, "restoreRate": 100 }
        }
      }
    }
  }
}
Field Meaning
requestedQueryCost What this request cost.
throttleStatus.scope The bucket closest to running out, USER or COMPANY. Pace your requests by this one.
maximumAvailable, currentlyAvailable, restoreRate That bucket’s capacity, points left after this request, and refill rate in points per second.
retryAfterMs How long to wait before retrying. It’s 0 unless the request was throttled.
buckets Both buckets, if you want to track them separately.

Errors

Throttled (HTTP 429)

When a request doesn’t fit in your budget, it isn’t run. You get HTTP 429 with a Retry-After header in seconds, and a THROTTLED error:

{
  "errors": [
    {
      "message": "Rate limit exceeded.",
      "extensions": {
        "code": "THROTTLED",
        "scope": "USER",
        "retryAfterMs": 1840
      }
    }
  ],
  "extensions": { "cost": { "...": "..." } }
}
  • retryAfterMs is the exact wait in milliseconds. Retry-After is the same wait rounded up to whole seconds.
  • scope: "USER" means your own budget ran out. Wait retryAfterMs and retry.
  • scope: "COMPANY" (“Company rate limit exceeded.”) means the shared company budget ran out. Other integrations are using it too, so wait retryAfterMs plus a little random jitter before retrying.

A throttled request doesn’t use any budget.

Query too expensive (HTTP 400)

An operation that costs more than the single-query limit is rejected with HTTP 400 and a QUERY_TOO_EXPENSIVE error, for example “Query cost 1251 exceeds the maximum allowed cost of 1000.” Retrying won’t help. Request smaller pages or fewer nested objects.

Other query limits

These apply to every request, whatever its cost:

  • Queries can be nested at most 7 levels deep.
  • A query can use at most 15 aliases.
  • Pages are limited to 100 records (first / last). See Pagination.

Tips

  • Ask only for what you use. Scalar fields are free, but each nested object adds a point, and inside a page that point is multiplied by the page size.
  • Keep page sizes reasonable. Lowering first from 100 to 25 cuts a paginated query’s cost by about four times. Follow pageInfo.endCursor for more.
  • Watch currentlyAvailable. Slow down before you reach zero. Don’t wait for a 429.
  • Retry on 429 and nowhere else. Wait retryAfterMs before retrying, and add jitter when scope is COMPANY.
  • Spread out bulk jobs. At the default refill rate of 50 points per second, a 151-point page takes just over 3 seconds to earn back. Fetching one every 4 seconds can run indefinitely. A full bucket also allows a short burst: 6 of those pages (906 points) fit in a 1,000-point bucket.