Skip to main content

Rate limits and request limits

ARC limits how often you can call it, how large a request can be, and how much work a single GraphQL operation may ask for. This page lists each ceiling, shows what a request that hits one looks like on GraphQL and REST, and gives a backoff recipe you can adapt.

The values below are defaults. Read the response headers and error bodies rather than hard-coding a number.

For the generic retry policy, idempotency keys and pagination shapes, see Errors, pagination, and idempotency.

At a glance​

LimitValueApplies toWhere it bites
Request throttle600 requests per 60 secondsEach client IP, per GraphQL root field or REST routeHTTP 429 on REST; a ThrottlerException error on GraphQL
Sign-in attempts5 per minuteEach client IP, sign-inA GraphQL error with extensions.status 429 and a retryAfter value
Password-reset requests3 per hourEach client IPA GraphQL error with extensions.status 429 and a retryAfter value
Registration10 per hourEach client IPA GraphQL error with extensions.status 429 and a retryAfter value
Some operations, mostly AI assistant onesTighter per-operation limits, 5 to 30 per minuteEach client IP, per operationSame headers and error shape as the request throttle
MCP server120 requests per minute by defaultEach organizationHTTP 429 with Retry-After; see MCP
Service API keyThe key's rateLimit requests per minute, 100 by defaultEach API key, across every /api/service/* routeHTTP 429 with code API_KEY_RATE_LIMITED and Retry-After; see API keys
GraphQL query depth12 levelsEach GraphQL operationRejected before it runs
GraphQL query size250 fields, counting fields reached through fragments and aliasesEach GraphQL operationRejected before it runs
JSON request body50 MBEach requestRefused; split the work
Request time-outAbout 60 secondsEach requestCut off after about 60 seconds
Page sizeproducts: at most 100 (larger values are capped). quotes: at most 100 in filter.limit (larger values are rejected). Service API lists: limit 1 to 100, default 50 (other values are rejected). Other lists: request 100 or fewerEach list queryA silent cap or a validation error
Idempotency retention48 hours by default; 30 days for createCustomerCollection, createSupplierPayment and createPartnerEach idempotency keyKeys are remembered for this long; reuse the same key for retries inside that window
Concurrent sessions5 per staff user by defaultEach staff userThe least recently used session is revoked
UploadsImage 5 MB; document 10 MB; logo 2 MBEach fileAn oversize file is refused with HTTP 413; see Files and exports
Product import100,000 rows and 64 MiB per fileEach importAn oversize import is rejected; see Bulk import

The request throttle​

Every GraphQL operation and REST route passes through a global throttle of 600 requests per minute. The count is kept per client IP address and per operation, where an operation is one GraphQL root field (query or mutation) or one REST route. In practice:

  • traffic from one IP address to one operation shares one bucket;
  • different operations do not share a bucket, and each root field in a multi-field GraphQL document counts against its own bucket;
  • many callers behind one NAT or proxy share a bucket.

ARC derives your IP address from the connection, not from a header you send. You cannot choose it with X-Forwarded-For.

The count is kept by each server instance rather than shared across the whole deployment, and it can reset when ARC is redeployed. The ceiling you observe can therefore be higher than 600. Treat 600 per minute as the ceiling, never as a guarantee of throughput.

When you exceed the limit, the bucket blocks for a full window, about 60 seconds from the moment you tripped it. Calls made during the block are rejected, and they do not extend it.

A few high-volume read queries, for example products and categoryChildren, are exempt from the throttle today. Design as if they were not. Exempt operations do not return the X-RateLimit-* headers. They still obey the page-size, time-out and depth limits.

Some operations carry tighter, per-operation limits. Most of them are AI assistant operations, which allow between 5 and 30 calls per minute depending on the operation. They return the same headers, with their own ceiling in X-RateLimit-Limit, and behave the same way when blocked.

What a throttled response looks like​

On every response from a throttled operation, ARC sets these headers:

  • X-RateLimit-Limit: the operation's ceiling, 600 unless the operation has a tighter limit.
  • X-RateLimit-Remaining: the requests left in the current window.
  • X-RateLimit-Reset: the seconds until the window resets.

Only a blocked response carries Retry-After, which is the number of seconds until the block ends. A blocked response has Retry-After but not the X-RateLimit-* headers.

GraphQL responses carry the same headers, because the throttle writes them to the HTTP response. Every response also has an x-request-id header. Log it for any failing response, and quote it when you report a problem.

These headers are readable by a server-side client. A browser script on another origin cannot read them, because ARC does not expose them to cross-origin scripts. A browser client must detect a throttle by the error message instead.

REST​

A blocked REST call returns HTTP 429. The headers look like this:

HTTP/1.1 429 Too Many Requests
Retry-After: 37
x-request-id: <request-id>

The body looks like this:

{
"statusCode": 429,
"timestamp": "<ISO-8601>",
"path": "<path>",
"method": "POST",
"message": "ThrottlerException: Too Many Requests",
"requestId": "<request-id>"
}

The body has no error or code field. Match on the status code and the Retry-After header.

A normal response from a throttled route carries the rate-limit headers:

HTTP/1.1 200 OK
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 412
X-RateLimit-Reset: 23

GraphQL​

A throttled GraphQL operation does not return the REST body above. Do not rely on the HTTP status line for GraphQL either. Read errors and the headers.

Check for a Retry-After header first. It is present only when the operation is blocked. Otherwise match the message ThrottlerException: Too Many Requests. The error has no extensions.status, so do not look for one. The path names the root field you called.

{
"errors": [
{
"message": "ThrottlerException: Too Many Requests",
"path": ["quotes"],
"extensions": { "code": "INTERNAL_SERVER_ERROR" }
}
]
}

Sign-in and account limits​

Sign-in, password-reset and registration use a separate limiter, applied per client IP address. It counts over a sliding window, and attempts made while you are limited still count:

  • sign-in: 5 attempts per minute;
  • password-reset requests: 3 per hour;
  • registration: 10 per hour.

These operations are GraphQL mutations, so the HTTP status line is not 429. When one of these limits is exceeded, the error looks like this:

{
"errors": [
{
"message": "Too many login attempts, please try again later",
"path": ["login"],
"extensions": {
"code": "INTERNAL_SERVER_ERROR",
"status": 429,
"originalError": {
"statusCode": 429,
"message": "Too many login attempts, please try again later",
"error": "Too Many Requests",
"retryAfter": 60
}
}
}
]
}

originalError.retryAfter is the length of the window in seconds: 60 for sign-in, and 3600 for the hourly limits. It is not the time remaining. This limiter sends no Retry-After header. The password-reset and registration messages are Too many password reset requests and Too many registration attempts.

Do not build sign-in retry loops. Wait out the window and tell your user to try again later.

Account lockout. Repeated wrong passwords lock the account. By default, five failures lock it for 15 minutes. A workspace security policy can change both values. The lock is disclosed with the ACCOUNT_LOCKED code only to a caller who then supplies the correct password. For anyone else the answer is the same Invalid credentials response as a wrong password, so do not build lock detection on the response. See Error reference.

Tokens and sessions. Access tokens last one hour. Refresh tokens last seven days and rotate when used. By default a user can hold five concurrent sessions, and the least recently used one is revoked when a new one starts. See Authentication.

Refresh while permissions cannot be resolved. If a token refresh fails with extensions.status 503 and the message Could not resolve account permissions. Please retry., ARC could not load the account's permissions at that moment. The refresh token is still valid. Retry with backoff, as described below.

MCP and API-key limits​

MCP. The MCP server allows 120 requests per minute per organization by default. The limit is shared by every user and agent in that workspace, and it is counted in a fixed one-minute window. A JSON-RPC batch counts one request per message.

When the limit is exceeded, ARC returns HTTP 429 with a Retry-After header. The JSON-RPC error's data carries code set to RATE_LIMITED, retryable set to true, and retryAfterSeconds. Operators can configure this limit, so treat 120 as the default. See MCP for the tools themselves.

API keys. Each API key has a rateLimit: the number of requests per minute it may make. It is an integer from 1 to 10000, set when the key is created, and 100 when none is given. ARC enforces it per key. Every request made with the key counts, reads and writes alike, on every /api/service/* route, and the count is shared across the whole deployment rather than kept per server instance. The count runs in a fixed one-minute window and starts again when the window ends. The global per-IP throttle described above still applies to key traffic as well.

Every response to a request made with a valid key carries these headers:

  • X-ApiKey-RateLimit-Limit: the key's requests per minute.
  • X-ApiKey-RateLimit-Remaining: the requests left in the current window.
  • X-ApiKey-RateLimit-Reset: the seconds until the window ends.

They are separate from the X-RateLimit-* headers of the global throttle, which can appear on the same response. Read each pair for its own budget.

A request over the key's limit is refused with HTTP 429 and a Retry-After header, and nothing is done:

HTTP/1.1 429 Too Many Requests
Retry-After: 18
X-ApiKey-RateLimit-Limit: 100
X-ApiKey-RateLimit-Remaining: 0
X-ApiKey-RateLimit-Reset: 18
{
"statusCode": 429,
"timestamp": "<ISO-8601>",
"path": "/api/service/customers",
"method": "GET",
"error": "Too Many Requests",
"code": "API_KEY_RATE_LIMITED",
"message": "This API key is limited to 100 requests per minute. Retry after 18 seconds.",
"retryAfterSeconds": 18,
"requestId": "<request-id>"
}

Refused requests still count, but they do not extend the window. Wait retryAfterSeconds (the same value as Retry-After) before the next request. To change a key's limit, create a replacement key with the new rateLimit; ARC has no operation that edits an existing key. A request refused before the key is accepted, for example with a 401, does not count and does not carry these headers.

Plan allowances are not rate limits. When your workspace reaches a plan allowance, such as its monthly quote allowance, ARC refuses the call with status 402 (HTTP 402 on REST, extensions.status 402 on GraphQL) and a message containing limit reached. That is a billing ceiling, not a throughput limit. Ask your Growmax account manager about a higher allowance. For the exact response shapes, see Error reference.

Request-shape limits (GraphQL)​

The exact fields and arguments are in the GraphQL reference. Two limits apply to the shape of each operation:

  • at most 12 levels of nesting;
  • at most 250 fields per operation, counting fields reached through fragments and aliases.

A query over either limit is rejected before it runs. Its error has extensions.code set to GRAPHQL_VALIDATION_FAILED, and the message reads like one of these:

Query is too deep: depth N exceeds maximum allowed depth of 12.
Query is too large: N fields exceeds maximum allowed field count of 250.

To fix it, split the query, request only the fields you use, and page your lists.

Send one operation per HTTP request. Array batching is not supported. To fetch several things at once, use aliases within the 250-field budget, or send several requests in parallel within the throttle.

GraphQL POST requests must send Content-Type: application/json. A request sent as text/plain or as a form is rejected with a message that begins This operation has been blocked as a potential Cross-Site Request Forgery (CSRF). Introspection and the playground are off in production; see GraphQL API.

Unknown fields are rejected, not ignored. An unknown field written in the document fails validation with GRAPHQL_VALIDATION_FAILED. An unknown key in an input object passed as a variable fails with BAD_USER_INPUT.

Size and time limits​

  • A JSON request body over 50 MB is refused. Split the work into smaller requests.
  • A request that gets no response for about 60 seconds is cut off. Keep uploads small, and page large reads.
  • An upload over its limit (image 5 MB, document 10 MB, logo 2 MB) is refused with HTTP 413 and the message File too large. It exceeds the maximum size for this upload. The per-route upload rules, including how upload URLs work, are in Files and exports.
  • A product import is limited to 100,000 rows and 64 MiB per file. An import over the row limit is rejected with a message such as Import has N rows; the maximum per import is 100000. Split the file and retry. See Bulk import.

Pagination limits​

Request at most 100 rows per page on every list.

  • products caps limit at 100 without an error. You get up to 100 rows and a nextCursor.
  • quotes rejects a filter.limit above 100 with a validation error (BAD_REQUEST).
  • Other list operations may not cap the page size. A very large page can run into the 60-second time-out.

For the shape of each list, see Pagination is operation-specific. Store nextCursor (or the operation's checkpoint) after each page, so that a throttle block costs you a pause rather than a restart.

As an upper bound, a sync that reads 100 rows per call at the full 600 requests per minute reads about 60,000 rows per minute from one throttled operation per IP address. This is derived from the limits, not a promise. products and categoryChildren are exempt from the throttle, so this bound does not describe them. A Service API sync is bounded by its key's rateLimit first: at the default of 100 requests per minute and 100 rows per page, one key reads at most about 10,000 rows per minute across all routes.

Back off and retry​

A throttle block lasts about 60 seconds, so a tight retry loop only burns requests. Use this order:

  1. Treat these as retryable: a REST response with status 429, 502, 503 or 504; a network time-out; a GraphQL response that carries a Retry-After header or an error with the message ThrottlerException: Too Many Requests; and a GraphQL error whose extensions.status is 429 or 503.
  2. If Retry-After is present, wait at least that many seconds, plus up to one second of jitter. It is a number of seconds, not a date. If it is absent, use exponential backoff with full jitter: wait a random time between 0 and the smaller of 60 seconds and 2 to the power of (attempt minus 1) seconds. Stop after about six attempts.
  3. Do not retry while a block is in force. Sleep until Retry-After has passed.
  4. For writes, send the same Idempotency-Key header (or the idempotencyKey input, where the mutation exposes it) on every attempt. Never generate a new key for each attempt. Keys are remembered for 48 hours by default, and for 30 days for createCustomerCollection, createSupplierPayment and createPartner. If you receive IDEMPOTENCY_IN_PROGRESS, the first attempt is still running. Wait briefly and retry with the same key. See Error reference.
  5. Cap your own concurrency. One slow request can hold a connection for up to 60 seconds. Keep each operation well below 600 requests per minute, for example by allowing no more than about 8 requests per second across all operations, and spread work across operations.
  6. Do not retry 400, 401, 402, 403, 404 or 409 responses, or validation failures, without changing the cause. IDEMPOTENCY_IN_PROGRESS is the only exception.
  7. Log the x-request-id of every failing response.

The helper below is a sketch for your own client. ARC does not ship it.

type ErrorBody = {
code?: string; // REST typed errors
errors?: { message?: string; extensions?: { code?: string; status?: number } }[];
};

const RETRYABLE_STATUS = new Set([429, 502, 503, 504]);
const THROTTLED = "ThrottlerException: Too Many Requests";
const IN_PROGRESS = "IDEMPOTENCY_IN_PROGRESS";

// Sketch only. `send` must reuse the same idempotency key on every attempt.
async function withBackoff(
send: () => Promise<Response>,
maxAttempts = 6,
): Promise<Response> {
for (let attempt = 1; ; attempt++) {
const res = await send();
const body = (await res.clone().json().catch(() => null)) as ErrorBody | null;
const retryableGraphQL =
body?.errors?.some(
(e) =>
e.message === THROTTLED ||
e.extensions?.code === IN_PROGRESS ||
e.extensions?.status === 429 ||
e.extensions?.status === 503,
) ?? false;
const retryable =
RETRYABLE_STATUS.has(res.status) ||
res.headers.has("retry-after") ||
body?.code === IN_PROGRESS ||
retryableGraphQL;
if (!retryable || attempt >= maxAttempts) return res;

const retryAfter = Number(res.headers.get("retry-after"));
const waitSeconds =
retryAfter > 0
? retryAfter + Math.random() // honour Retry-After, plus jitter
: Math.random() * Math.min(60, 2 ** (attempt - 1)); // full jitter
await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1000));
}
}

What not to do​

  • Do not share one API key between several workers and expect each to get the full rateLimit. The limit is per key, across all its requests. Pace the workers together, and watch X-ApiKey-RateLimit-Remaining.
  • Do not spread one stream across many IP addresses to get around the throttle, and do not send a forged X-Forwarded-For. ARC derives the IP address itself. If your integration needs more throughput, ask your Growmax account manager.
  • Do not retry in a loop during a block. Honour Retry-After.
  • Do not create a new idempotency key for each retry.
  • Do not use X-RateLimit-Remaining for global accounting. It is counted per server instance.
  • Do not build sign-in retry loops. Sign-in is limited to 5 attempts per minute per IP address, and accounts lock after repeated failures.
  • Do not request pages of more than 100 rows, do not build queries deeper than 12 levels or wider than 250 fields, and do not send several operations in one JSON array.
  • Do not send GraphQL as a form or as text/plain.

Next: Error reference, with the exact shapes of the errors described on this page.