# Exceeded 2 calls per second for api client

*Shopify · error message*

**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 bucket holds **40 requests** on standard plans.
- It drains at **2 requests per second**.
- Every request you make fills it by one.
- When the bucket is full, further requests get **HTTP 429** with a `Retry-After` header.

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

| Approach | Effect |
|---|---|
| Honour `Retry-After` | The API tells you exactly how long to wait. Use that number. |
| Read the bucket state from response headers | Slow down before you hit the wall rather than after |
| Pace client-side with a token budget | Requests queue at a sustainable rate instead of racing |
| Exponential backoff with jitter | Prevents several workers from retrying in lockstep |
| Move bulk reads to GraphQL | Cost-based limits, far more data per call |
| Idempotency keys on writes | A 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.

- **Pacing is a property of the pipeline, not of each script.** One place decides how fast anything talks to Shopify, so a new job cannot accidentally starve an existing one.
- **429 is expected, monitored, and not alarming.** What we alert on is throughput falling below what the day's volume requires, because that is the condition that actually costs you something.
- **Writes are idempotent by construction.** Retrying is safe, which is what makes aggressive backoff acceptable in the first place.
- **We watch the API's own changelog daily.** A limit change or an endpoint deprecation reaches us as a scheduled piece of work, not as a stalled sync on a Monday morning.

## 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.

## Integrations this affects

- [Shopify ↔ DATEV](https://seamless.engineering/integrations/shopify-datev/): Shopify DATEV integration
- [Shopify ↔ NetSuite](https://seamless.engineering/integrations/shopify-netsuite/): Shopify NetSuite integration
- [Shopify ↔ sevDesk](https://seamless.engineering/integrations/shopify-sevdesk/): Shopify sevDesk integration
- [Shopify ↔ lexoffice](https://seamless.engineering/integrations/shopify-lexoffice/): Shopify lexoffice integration
- [Shopify ↔ Business Central](https://seamless.engineering/integrations/shopify-business-central/): Shopify Business Central integration

## Request a scoping call

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.

- Email: hello@seamless.engineering
- Contact form: https://seamless.engineering/#contact
