# 429 Too Many Requests

*Shopify · error message*

**In short:** HTTP 429 means you have exceeded the request budget. On Shopify this is a leaky bucket of 40 requests draining at 2 per second on standard plans, and the response carries a Retry-After header telling you how many seconds to wait. The cause that surprises people is that the budget is per API client, not per script: a scheduled sync, a webhook handler and a third-party app all drawing on the same credentials share one bucket, so a quiet store can still return 429 while nobody appears to be doing anything.

## What the response tells you

A 429 is Shopify saying: the request was well formed, you are authenticated, and you have used your budget. It is not an error in your data and not an outage.

The response normally includes a `Retry-After` header with the number of seconds to wait. That number is authoritative and better than any delay you might choose yourself.

Underneath it is a leaky bucket: 40 requests capacity on standard plans, draining at 2 per second. Shopify Plus gets more headroom, and the GraphQL Admin API uses a cost-based model rather than a request count.

## The cause everyone misses

**The budget belongs to the API client, not to your script.**

This is why teams see 429s on stores where, as far as anyone can tell, nothing is happening. In practice a single set of credentials is often shared by:

- a scheduled catalogue or order sync,
- a webhook handler reacting to store events,
- a backfill or migration job somebody left running,
- a third-party app installed with the same access,
- a developer testing against production.

Each looks quiet on its own. Together they saturate one bucket. Nobody is doing anything obviously wrong, and the error still appears.

If you are seeing unexplained 429s, enumerate everything that authenticates with those credentials before you look at your own code. The answer is usually in that list.

## Reading the situation correctly

| Symptom | Likely cause |
|---|---|
| 429s during a bulk job only | Sustained rate exceeds 2/s, expected without pacing |
| 429s at a fixed time each day | Two scheduled jobs overlapping |
| 429s with no obvious activity | Shared credentials across several consumers |
| 429s that got worse after a retry fix | Retries are too aggressive and self-reinforcing |
| 429s right after installing an app | The app shares your budget |

## Handling it properly

1. **Honour `Retry-After`.** It is a real answer, not a suggestion.
2. **If the header is absent, back off exponentially with jitter.** Jitter matters as soon as more than one worker exists, otherwise they all retry at the same instant and recreate the spike.
3. **Cap the number of retries** and surface what still fails after the cap. Infinite retry is how a small problem becomes a silent one.
4. **Make writes idempotent.** This is the difference between a retry being safe and a retry creating a duplicate invoice.
5. **Pace proactively.** Response headers report remaining capacity. Slowing down before the bucket empties is cheaper than recovering after it does.
6. **Give every consumer of the API one shared limiter**, so a new job cannot starve an existing one.

## What we do differently

- **One limiter, not one per script.** Everything that talks to a given store passes through the same paced queue, so total draw is bounded by design rather than by hope.
- **429 is a metric, not an alarm.** We alert when throughput falls below what the day's volume needs, which is the condition that actually delays your data. A handful of 429s with the work still completing on time is not worth waking anyone.
- **Every write carries an idempotency key.** Retrying is safe, which is what allows the backoff to be aggressive without risking duplicate orders or refunds.
- **Backlogs drain deliberately.** After an outage the catch-up is throttled instead of racing, because the fastest way to stay rate limited is to try to make up time immediately.

## Frequently asked questions

### We get 429s and there is barely any activity on the store. How?

Because the limit applies to the API client, not to a single process. If a nightly sync, a webhook consumer and an installed app all authenticate with the same credentials, they draw on one shared bucket. Each one looks idle in isolation while collectively saturating the limit. This is the single most common cause of 429s that appear inexplicable.

### Retry-After is empty. What now?

Fall back to exponential backoff with jitter: wait a second, then two, then four, up to a sensible ceiling, with a small random offset so parallel workers do not retry in lockstep. Never fall back to retrying immediately. Treat a missing header as unknown, not as zero.

### Is a 429 a failure?

No, it is flow control, and treating it as a failure causes real harm. Integrations that log every 429 as an error bury genuine failures in noise, and teams stop reading the alerts. Count 429s as a rate metric and alert on throughput, not on their existence.

### Can retrying a 429 duplicate data?

It can, and this is the risk worth taking seriously. If a write times out or is rejected after Shopify has already processed it, a naive retry creates a second record. Use idempotency keys on writes so a repeat is recognised. Getting this wrong on order or refund creation produces duplicate financial records that are far more expensive than the rate limit ever was.

### How do we stop hitting the limit at all?

By pacing before the limit rather than reacting after it. Read the remaining bucket capacity from response headers, queue requests through one shared limiter, and move bulk reads to the GraphQL Admin API. The goal is that 429 becomes rare enough to be interesting rather than routine.

## Integrations this affects

- [Shopify ↔ DATEV](https://seamless.engineering/integrations/shopify-datev/): Shopify DATEV integration
- [Shopify ↔ sevDesk](https://seamless.engineering/integrations/shopify-sevdesk/): Shopify sevDesk integration
- [Shopify ↔ lexoffice](https://seamless.engineering/integrations/shopify-lexoffice/): Shopify lexoffice integration
- [Shopify ↔ NetSuite](https://seamless.engineering/integrations/shopify-netsuite/): Shopify NetSuite integration
- [Shopify ↔ Klaviyo](https://seamless.engineering/integrations/shopify-klaviyo/): Shopify Klaviyo 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
