TECHNICAL · INTEGRATIONS · APPS · 12 AUGUST 2025 · 9 MIN READ
Rate limits: designing an integration that does not throttle
Throttling is rarely a limit problem. It is an architecture problem — several jobs sharing one bucket, polling for data that Shopify would have pushed to you.
Shopify gives each app a leaky bucket per store, and every job you run against that store draws from the same one. The GraphQL Admin API meters calculated query cost in points, restoring at 100 points per second on standard plans, 200 on Advanced, 1,000 on Plus and 2,000 on enterprise, with a hard ceiling of 1,000 points for any single query. The legacy REST Admin API meters request count instead: a 40-request bucket leaking at 2 per second on standard plans, 400 leaking at 20 on Plus. An integration that throttles has almost always made one of three architectural mistakes — it polls for changes Shopify would have pushed, it runs batch work through the same lane as interactive work, or it has more than one process writing to a store with no shared view of the budget.
IN SHORT
- The bucket is scoped to your app on that store, so every job you run shares it — the nightly sync and the customer-facing lookup are competing.
- GraphQL Admin restore rates are 100 points per second on standard plans, 200 on Advanced, 1,000 on Plus and 2,000 on enterprise; one query is capped at 1,000 points on every plan.
- The REST Admin API has been a legacy API since 1 October 2024 and meters requests, not cost: 40 requests leaking at 2 per second on standard plans, 400 at 20 on Plus.
- Shopify states that Storefront API traffic from real buyers is not subject to a fixed request-per-minute limit, so a busy storefront is not the thing to engineer around.
- A single queue per shop with a concurrency limit solves more throttling than any retry strategy, because it makes the shared budget visible to every job.
- Some limits are not rate limits at all — stores past 500,000 variants are capped at 10,000 new variants a day on any API.
One bucket, every job
The single most useful fact about Shopify rate limits is not a number. It is that the bucket belongs to your app on that store, and nothing in the platform knows which of your processes deserves it more. The REST documentation states the limit plainly as requests per app, per store. The GraphQL bucket works the same way, which is why throttleStatus.currentlyAvailable can be near zero on a call your service has not made — another one of your own processes drained it.
This is where most throttling incidents actually begin. A store has a nightly catalogue sync, a webhook consumer writing fulfilment updates, a customer service tool that reads orders on demand, and a reporting job somebody scheduled hourly. Each was built in isolation, each behaves well on its own, and together they spend a budget none of them can see. The symptom is intermittent and lands on whichever job happens to be running when the others peak — which is why the ticket always blames the wrong component.
So the first architectural decision is not the retry policy. It is: what does the budget for this store belong to, and which component owns it?
Three APIs, three different limiters
Reasoning about a Shopify integration means holding three metering models at once, because they do not behave alike.
- GraphQL Admin API — metered by calculated query cost. Scalar and enum fields cost nothing, object fields cost one point, connection fields are priced from the
first/lastargument before execution, and mutations carry a base cost of ten. Restore rates are 100 points per second on standard plans, 200 on Advanced Shopify, 1,000 on Shopify Plus and 2,000 on Shopify for Enterprise. Any single query is capped at 1,000 points regardless of plan. - REST Admin API — metered by request count, and legacy since 1 October 2024; Shopify states all apps and integrations should be built with the GraphQL Admin API. The bucket is 40 requests leaking at 2 per second on standard plans and 400 leaking at 20 per second on Plus, with each response carrying
X-Shopify-Shop-Api-Call-Limitin the form32/40. - Storefront API — not metered the same way at all. Shopify documents that requests from real buyers are not subject to a fixed request-per-minute limit, that bots and crawlers are limited, most strictly when unsigned, and that there is a separate per-minute throttle on checkout creation that returns a
Throttlederror.
Stop polling for things Shopify will send you
The highest-value change in most throttling reviews is deleting a loop. A job that asks every fifteen minutes whether any order has changed is spending budget continuously to discover, most of the time, that nothing has. Multiply that by four integrations and a store’s entire budget goes on questions with the answer “no”.
The replacement is the boring one: subscribe to the webhook topic and consume events. The read budget then gets spent on the records that actually changed, and it scales with the store’s real activity rather than with your polling interval. Webhooks come with their own failure mode — delivery is not guaranteed and retries mean you will process some events twice — which is a design problem worth solving once rather than a reason to keep polling.
Where webhooks are not the answer, bulk operations usually are. Anything whose shape is “every product” or “every order since the beginning” should be submitted as an asynchronous bulk operation rather than paged, because bulk operations are not subject to the per-query cost cap or the ordinary rate limits. A paginated full-catalogue read is a recurring incident on any store large enough to care.
What is left after removing polling and bulk work is usually small: targeted reads, targeted writes, and reconciliation. That residue fits inside the budget comfortably on any plan.
One queue per shop, with a concurrency limit
When several jobs must share a budget, the fix is to make them share a lane. Route every Admin API call for a given store through one queue, give the queue a concurrency limit, and have the worker read extensions.cost.throttleStatus from each response so the queue knows what is left rather than guessing.
That one pattern removes most of the failure modes. Jobs stop competing because they are serialised. Backpressure becomes visible — a queue depth you can graph — instead of arriving as a scatter of 429s in an error tracker. And the pacing adapts on its own when a merchant upgrades to Plus and the restore rate jumps tenfold, because the worker is reading the real restoreRate rather than a delay somebody tuned against a development store two years ago.
Give the queue priorities while you are there. An interactive request — a support agent opening an order, a customer checking a status — should not wait behind 40,000 queued catalogue writes. Two lanes with a reserved share of the budget for the interactive one costs very little to build and is the difference between a slow batch job and a broken product.
The one thing to resist is scaling horizontally without a shared limiter. Four workers each politely observing the throttle status they personally saw will still collectively overrun the bucket, because none of them knows about the other three. The limiter has to live outside the worker.
Back off on the numbers you were given
You will still be throttled occasionally, and that is fine — the goal is an integration that degrades rather than one that never hits the limit. What matters is what happens next.
Both Admin APIs return 429 Too Many Requests. REST includes a Retry-After header carrying the seconds to wait, and Shopify’s general guidance gives one second as the recommended backoff. On GraphQL you can do better than a fixed wait: throttleStatus gives maximumAvailable, currentlyAvailable and restoreRate, so the wait is simply the shortfall divided by the restore rate. For Storefront API checkout throttling, Shopify recommends a request queue with an exponential backoff algorithm.
Two things to get right regardless of the API. Retries need jitter, or a burst of failures becomes a synchronised burst of retries that reproduces the problem exactly one second later. And any retried mutation needs to be safe to replay, because at some point one will be sent twice — the same idempotency problem as webhook redelivery, and worth designing once for both.
If you are hunting for what a query actually costs, the Shopify-GraphQL-Cost-Debug=1 header returns a per-field breakdown. That is a faster route to the expensive nested connection than reading the query and reasoning about it.
The limits that are not rate limits
A few documented ceilings return the same errors as throttling and are not fixed by slowing down. They are worth knowing before you spend a day tuning a queue that was never the problem.
- Variant creation on very large stores. Once a store passes 500,000 product variants, no more than 10,000 new variants can be created per day, on any API. A first-time import into a store that size needs a schedule, not a faster worker.
- Deep offset pagination on REST. A GET request with an offset beyond 100,000 returns 429. Page-based pagination is deprecated; use cursors.
- Tokenless Storefront access carries a query complexity limit of 1,000, and exceeding it returns
MAX_COMPLEXITY_EXCEEDEDrather than a throttle. - Security rejections. A request the platform considers malicious comes back as
430 Shopify Security Rejection, which is not a capacity signal and will not clear by waiting. - Partner and App Events APIs have their own request-based limits, scoped per API client and per app respectively — separate budgets from the store’s.
What we look at first in a review
When a client brings us an integration that throttles, we do not start with the code. We start with an inventory: every process that talks to this store, what it does, how often, and who owns it. That list is usually longer than anyone expected and contains at least one job whose original purpose nobody can remember.
Then three questions. Which of these is polling for something a webhook would deliver? Which is reading the whole catalogue when it needs a delta? And which two are running at the same time, every night, because they were both scheduled for midnight by different people?
The fix that follows is almost never a bigger plan. Upgrading to Plus multiplies the restore rate, and an integration with the wrong shape will find a way to exhaust that too — the same job, finishing its unnecessary work ten times faster, on a store ten times larger.
Questions this raises
How do Shopify API rate limits work?
Shopify uses a leaky bucket per app, per store. The GraphQL Admin API meters calculated query cost in points, restoring at 100 per second on standard plans, 200 on Advanced, 1,000 on Plus and 2,000 on enterprise, with a 1,000-point ceiling on any single query. The legacy REST Admin API meters request count instead — 40 requests leaking at 2 per second on standard plans, 400 at 20 per second on Plus.
Why does my integration throttle when each job looks fine on its own?
Because they share one bucket. The budget is scoped to your app on that store, not to a process, so a nightly sync, a webhook consumer and an on-demand lookup all draw from the same balance with no visibility of one another. Route every call for a store through a single queue with a concurrency limit and the competition disappears.
Does upgrading to Shopify Plus fix throttling?
It raises the restore rate to 1,000 points per second, which helps, but it does not change the 1,000-point cap on a single query and it does not fix an integration that polls for changes or reads the whole catalogue nightly. A badly shaped job on Plus simply does its unnecessary work faster.
Should I use webhooks or polling?
Webhooks, wherever the topic exists. Polling spends budget continuously to discover that nothing changed, and it scales with your interval rather than with the store’s activity. Budget for the webhook failure modes instead — delivery is not guaranteed and retries mean some events arrive twice, so consumers need to be idempotent.
How should I back off when I get a 429?
On GraphQL, read `extensions.cost.throttleStatus` and wait the shortfall divided by `restoreRate`. On REST, honour the `Retry-After` header; Shopify’s general recommended backoff is one second. Add jitter in both cases, or a burst of failures turns into a synchronised burst of retries.
Is the Storefront API rate limited?
Not in the same way. Shopify documents that requests from real buyers are not subject to a fixed request-per-minute limit. Bots and crawlers are limited, most strictly when unsigned, and checkout creation has its own per-minute throttle that returns a `Throttled` error — for which Shopify recommends a request queue with exponential backoff.
NEXT STEP
Free store audit
A senior Shopify engineer reviews your storefront, theme performance and checkout, then sends a prioritised list of fixes.
