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:
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.
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:
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": { "...": "..." } }
}
retryAfterMsis the exact wait in milliseconds.Retry-Afteris the same wait rounded up to whole seconds.scope: "USER"means your own budget ran out. WaitretryAfterMsand retry.scope: "COMPANY"(“Company rate limit exceeded.”) means the shared company budget ran out. Other integrations are using it too, so waitretryAfterMsplus 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
firstfrom 100 to 25 cuts a paginated query’s cost by about four times. FollowpageInfo.endCursorfor more. - Watch
currentlyAvailable. Slow down before you reach zero. Don’t wait for a429. - Retry on
429and nowhere else. WaitretryAfterMsbefore retrying, and add jitter whenscopeisCOMPANY. - 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.