← All error messages
Shopify // error message

Exceeded 2 calls per second for api client

In short

Shopify's REST Admin API uses a leaky bucket: you get a bucket of 40 requests that drains at 2 per second on standard plans. You can burst to 40, but sustained throughput is 2 per second, and going over returns HTTP 429 with a Retry-After header. Almost every integration hits this the first time it syncs a real catalogue, because it was built and tested against a store with fifty products. The fix is not faster retries, it is pacing: honour Retry-After, keep a token budget client-side, and move bulk work to the GraphQL Admin API where cost-based limits let you fetch far more per call.

What the limit actually is

Shopify’s REST Admin API meters requests with a leaky bucket:

The distinction that catches people out is between burst and sustained capacity. You can fire 40 requests instantly. You cannot fire 40 requests per second. Sustained throughput is 2 per second, and the bucket of 40 only buys you twenty seconds of that rate before you are pinned to the drain rate.

Why it always shows up at go-live

Integrations get built against development stores. A development store has fifty products and a dozen orders, so the bucket never fills and the limit never appears.

Then the same code points at a store with 20,000 products and a year of order history, and the arithmetic becomes visible: at 2 requests per second, 20,000 sequential requests take nearly three hours, assuming nothing else is competing for the same budget.

Nothing broke. The code was always this shape. The volume simply made the shape matter.

What not to do

Do not retry immediately. A retry is a request, and a request fills the bucket you are waiting on. Tight retry loops keep you permanently at the limit and turn a brief slowdown into a stall.

Do not use a fixed sleep. A hard-coded one-second pause is either too slow when there is headroom or too fast when there is not, and it ignores the answer the API already gave you.

Do not treat 429 as an error. It is flow control. An integration that logs it as a failure will fill its error tracking with noise and hide the failures that matter.

What to do instead

ApproachEffect
Honour Retry-AfterThe API tells you exactly how long to wait. Use that number.
Read the bucket state from response headersSlow down before you hit the wall rather than after
Pace client-side with a token budgetRequests queue at a sustainable rate instead of racing
Exponential backoff with jitterPrevents several workers from retrying in lockstep
Move bulk reads to GraphQLCost-based limits, far more data per call
Idempotency keys on writesA retried write cannot create a duplicate

That last row is the one people skip, and it is the one that causes real damage. Rate limiting turns a single logical write into several attempts. Without an idempotency key, a retry after a timeout can create a second order, a second invoice, a second refund. The rate limit is an inconvenience; a duplicate financial document is an accounting problem.

Why this is an operations problem, not a coding problem

Rate limits are not a bug to be fixed once. They are a permanent condition of the environment that shifts underneath you: Shopify changes limits, moves functionality between REST and GraphQL, and deprecates API versions on a schedule. A sync that is paced correctly today can be wrong in six months because the endpoint it depends on was reshaped.

Frequently asked questions

Why does it work in development and fail in production?
Because a development store has a handful of products and orders, and a real one does not. At 2 requests per second sustained, a full catalogue sync is measured in hours, not minutes. Nothing about the code changed between the two environments, only the volume, which is why this failure tends to arrive on the day of go-live rather than during testing.
Can I just retry immediately?
That is the worst response available. Immediate retries consume the bucket you are waiting to refill and keep you pinned at the limit. The 429 response carries a Retry-After header giving the number of seconds to wait. Honour it. If the header is missing, back off exponentially rather than guessing a fixed delay.
Does Shopify Plus solve it?
It raises the limits, it does not remove them. If your integration has no pacing, a higher ceiling just moves the point at which it falls over. Plans are a capacity decision, pacing is a correctness decision, and the second one does not go away when you buy more of the first.
Should we move to GraphQL?
For bulk work, yes. Shopify has been steering bulk operations toward the GraphQL Admin API, which uses cost-based limits instead of a request count. The practical benefit is that one well-shaped query can return what would have taken dozens of REST calls, so you fetch more data for less of your budget. It is not a bypass of rate limiting, it is a better unit of work.
How would you build this?
With a client-side budget that never lets the bucket empty, rather than a client that discovers the limit by hitting it. Requests go through a paced queue, the remaining bucket capacity from the response headers feeds back into that pacing, and bulk reads use GraphQL. Retries carry an idempotency key so a repeated write cannot create a duplicate order or invoice.
// scoping request

Seeing this in production and want it to stop being your problem? We design, build, and permanently operate the pipeline, including the validation and retry logic that stops this class of failure reaching you. Fixed-price scoping quote within 48 hours.

We reply within one working day. No newsletter, no sales sequence.