Skip to main content

Rate limits

Every GraphQL request is assigned a numeric cost before it is executed: the complexity score of its query. Each merchant has a budget that refills at a steady rate up to its maximum bucket size. As long as a request fits into the currently available budget, it is executed and its cost is deducted; otherwise it is rejected with a THROTTLED error.

Rate limits are tracked per merchant, not per API token. Creating additional tokens does not raise the budget.

Budgets by plan​

The API is available on the Professional and Enterprise plans. Each of those plans has a bucket size (the maximum budget available at any moment) and a restore rate (how quickly the budget refills, in points per second). Short bursts are allowed up to the bucket size; sustained throughput is capped by the restore rate.

PlanRestore rateBucket size
Professional500 points/s5,000 points
Enterprise2,000 points/s20,000 points

A merchant on the Professional plan, for example, can burst up to 5,000 points of request cost from a full bucket and sustain about 500 points/s of cost on average once the bucket has been drawn down.

Limits per request​

Independent of the bucket, every request has to stay within two limits:

  • Complexity: the complexity score of the query may be at most 5,000.
  • Depth: fields may be nested at most 15 levels deep.

A query above one of these limits is rejected during validation with a BAD_REQUEST error, before any cost is charged. A request whose cost is larger than the merchant's bucket size can never fit into the bucket and is rejected with the code MAX_COST_EXCEEDED.

In both cases, retrying with the same shape fails again. Split the operation into smaller pages or query a smaller set of fields.

Inspecting the current budget​

Every response to a request that passed validation carries the state of the bucket in three headers:

HeaderContent
X-RateLimit-LimitThe bucket size of the merchant.
X-RateLimit-RemainingThe points left in the bucket after this request.
X-RateLimit-ResetThe seconds until the bucket is full again.

When the response contains data, it also reports the request's cost and the bucket state in the GraphQL extensions object:

{
"extensions": {
"cost": {
"requestedQueryCost": 350,
"throttleStatus": {
"maximumAvailable": 5000,
"currentlyAvailable": 4650,
"restoreRate": 500
}
}
}
}
  • requestedQueryCost — the cost calculated for the request before execution and charged against the bucket.
  • throttleStatus.maximumAvailable — the bucket size for this merchant.
  • throttleStatus.currentlyAvailable — points remaining in the bucket right after this response.
  • throttleStatus.restoreRate — points per second added to the bucket until it is full.

Clients that batch synchronization work can use currentlyAvailable and restoreRate, or the X-RateLimit-* headers, to pace requests instead of relying on retries.

Handling throttling​

When a request does not fit into the current budget, the API returns a GraphQL error with the THROTTLED code and a retryAfter field in the error's extensions. The response also carries the request's cost and the bucket state in extensions.cost:

{
"errors": [
{
"message": "Throttled. Retry in 1.7s — your requested cost (1200) exceeds currently available points (350).",
"extensions": {
"code": "THROTTLED",
"retryAfter": 1.7
}
}
],
"extensions": {
"cost": {
"requestedQueryCost": 1200,
"throttleStatus": {
"maximumAvailable": 5000,
"currentlyAvailable": 350,
"restoreRate": 500
}
}
}
}

retryAfter is the number of seconds the client should wait before sending the next request, at least one second. The Retry-After header carries the same wait rounded up to whole seconds. Back off until that time has elapsed before retrying.

Reducing request cost​

Cost grows with the number of items returned by list fields and with the depth of nested connections, so the most effective ways to lower cost are:

  • Request only the fields the integration needs; drop nested connections that are not used.
  • Use smaller page sizes on list fields, and paginate explicitly with first + after.
  • Cache stable objects such as warehouses, bin locations, and suppliers locally where possible.
  • Prefer incremental synchronization by filtering on fields such as updatedAt when the relevant type exposes them.