Rate limits, and building a shop that stays inside them
6 min read
Sixty a minute per key by default, read from a header rather than hard-coded — and a 429 that carries no Retry-After, so your client has to have been watching on the way down.
The default allowance is sixty requests a minute, it is counted per key rather than per account, and going over it answers 429 rate_limited. Two details decide whether your client handles that well, and neither is obvious from the status code: the allowance is a property of your account and may not be sixty, so read it from X-RateLimit-Limit rather than hard-coding it — and the 429 itself carries no `Retry-After` and no rate-limit headers at all, so you have to have been reading them on the way down.
Where the number comes from
The ceiling is set on your account. Where nobody has set one it resolves to the platform default of sixty a minute, and where a higher one has been agreed for you that is the one that applies. It is never written into your account as a copy of the default, which is why raising the default raises it for everybody at once rather than pinning you to today's figure.
Practically: send one request, read X-RateLimit-Limit off the response, and use that as your budget. A client with 60 compiled into it is a client that will not notice the day you are given more.
The bucket is the key, not the account
Two keys on one account each get the full allowance, and that is deliberate rather than an oversight to be tidied up. It is also the cleanest tool you have for protecting the path that matters:
- issue one key for placing orders, used only by your checkout;
- issue another for background work — the nightly catalogue sync, a balance poll, a reconciliation job.
Then a sync that runs long, or a reconciliation that walks ten pages of orders, cannot spend the allowance a customer's purchase needs. Keys are managed from your dashboard, one per holder; issuing a new key for a holder revokes the previous one immediately, so rotate deliberately rather than by accident.
The headers, and the one that is missing
Every request that is answered carries two headers:
-
X-RateLimit-Limit— your ceiling for the window. -
X-RateLimit-Remaining— what is left of it.
The 429 carries neither, and it carries no Retry-After and no reset timestamp either. It is the error envelope and nothing else. So design for that rather than against it:
- Keep the last `Remaining` you saw, per key, in your client. That is your only live signal.
- When the refusal arrives, back off on your own clock. The window is a minute, so a first wait in that order of magnitude with jitter is reasonable; do not busy-retry, because every rejected attempt is another request against a limiter that is already saying no.
- Do not parse the message to work out how long to wait. It says you went over the limit and nothing about when the window turns.
Design so you rarely go near it
- Cache the catalogue. It is a list that changes when somebody changes it, not a per-request read. Sync it on a schedule and order from your own copy of the SKUs.
-
Use webhooks instead of polling. An order comes back
pendingand completes in the background; subscribing toorder.deliveredreplaces a poll per order with one request when it matters — see handling webhooks. - Poll the balance on a timer, not per checkout. Your balance does not change between two customers often enough to justify a request each time.
- Serialise through one client. Parallel workers multiply your request rate by the number of workers, and the limiter counts all of them.
- Treat `Remaining` as a budget with a floor. When it drops below a number you choose, queue non-urgent work instead of sending it, and let orders through.
- Retry a 429 with the same `Idempotency-Key` as the attempt it is retrying. A retry that re-keys is not a retry — see retrying an order without buying it twice.
What a 429 costs, and what it does not
Nothing was charged and nothing was created: the request never reached the order path. So the refusal itself is safe, and the risk is entirely in what your own system does with it — a checkout that gives up and shows an error is a lost sale where a checkout that queues and retries is a few seconds of waiting.
This is also why a queue in front of your ordering path is worth more than a clever retry loop inside it: the customer is not holding a connection open while you wait out a window.
What is not a rate limit
Backing off is the right answer to exactly one refusal. Every other one is either a bug in the request or a state of the world, and waiting does not change either:
-
402 insufficient_balance— top the wallet up; retrying will not fill it. -
409 package_unavailable— the denomination is off sale; re-read the catalogue on your schedule. -
422 …— fix the request. -
401/403— a person has to act.
Each of them, with what to do, is in every API error and what to do about it.
Questions
Can I ask for a higher allowance?
Yes — it is a figure on your account, and a shop with a genuine reason for more is the case it exists for. Whatever is agreed shows up in X-RateLimit-Limit on your next response.
Do reads and writes share the allowance?
They share the key's. That is the argument for separating keys by job: a catalogue sync and a checkout on one key are spending one budget.
What happens to the count if a request fails with a 4xx?
It was still a request. Design your client so a loop of failing requests cannot run unattended — a 422 retried in a tight loop becomes a 429 as well, and then you have two problems.
Is there a daily or monthly cap as well?
The limit described here is the one that applies: a per-minute ceiling counted per key. There is no separate quota to budget against.
Last updated