LUCENTCOMMERCEGET A FREE STORE AUDITFREE AUDIT

TECHNICAL · APPS · DATA · 25 FEBRUARY 2025 · 9 MIN READ

The Admin GraphQL API: pagination, cost and bulk operations

Most Shopify integrations are throttled because they ask for too much per query, then retry harder. Read the cost object and the problem goes away.

A request and its response, side by side

Query only the fields you need, page with cursors rather than offsets, read extensions.cost on every response instead of guessing, and move anything that touches the whole catalogue to a bulk operation. The Admin GraphQL API is not rate-limited by request count but by calculated query cost in points, drawn from a bucket that refills continuously — 100 points per second on standard plans, 200 on Advanced, 1,000 on Plus and 2,000 on enterprise. A single query cannot exceed 1,000 points regardless of plan. Almost every "Shopify is throttling us" ticket is a client asking for more connection nodes per page than it needs.

IN SHORT

  • Cost is calculated from the shape of the query, not the size of the response — asking for 250 nodes costs the same whether 250 come back or two do.
  • The bucket refills continuously, so bursts are fine as long as average cost stays under the restore rate.
  • Restore rates: 100 points/second standard, 200 Advanced, 1,000 Plus, 2,000 enterprise. One query is capped at 1,000 points on every plan.
  • `extensions.cost.throttleStatus` tells you exactly what is left. Read it and pace yourself; do not hardcode a delay.
  • Bulk operations do not carry the per-query cost cap or the rate limits, and are the right tool for any full-catalogue read or write.

Cost is about the query, not the data

The thing that surprises people coming from REST: the Admin GraphQL API charges you for what you asked for, before it knows what exists. Requesting first: 250 on a connection is priced as 250 nodes whether the store has 250 matching orders or none. Nested connections multiply — 250 orders each with 100 line items is not 250 units of work, it is the product of the two.

This is why the fix for throttling is almost never "add a longer sleep". It is to ask for less: fewer nodes per page, fewer nested connections, and only the fields you will actually use. A query that fetches the whole Order type because it was easier than listing fields is paying for every one of them.

The API returns both numbers. requestedQueryCost is what it charged you up front; actualQueryCost is what it settled on after execution, and the difference is refunded to your bucket. If your requested cost is consistently far above the actual, you are reserving capacity you never use — and being throttled for it.

Read the throttle status instead of guessing

Every response carries an extensions.cost object, and throttleStatus inside it has three fields worth building against: maximumAvailable, currentlyAvailable, and restoreRate.

That is a live view of your own budget, which means the correct pacing strategy writes itself: before the next call, check whether currentlyAvailable covers the cost of the query you are about to send, and if it does not, wait the difference divided by restoreRate. No magic numbers, no environment variable full of milliseconds that somebody tuned against a development store two years ago and nobody has touched since.

It also means your integration adapts on its own when the merchant upgrades to Plus and the restore rate jumps tenfold, rather than crawling at the pace you hardcoded for a standard plan.

  • maximumAvailable — the size of the bucket.
  • currentlyAvailable — what is left right now.
  • restoreRate — points returned per second, so wait time is (needed − available) ÷ restoreRate.

Pagination: cursors, always

Connections page with first/after and last/before, and each edge carries a cursor. You read pageInfo { hasNextPage endCursor } and pass endCursor into the next call. There is no page number and there should not be — offset pagination over a catalogue that is being edited while you read it silently skips and duplicates records.

Two practical rules. First, pick a page size deliberately: 250 is the maximum on most connections and is rarely the right answer, because it is also the most expensive and the slowest to fail. Fifty is a sane default for a job that will run for a while. Second, persist the cursor. A sync that dies at record 40,000 and restarts from the beginning is a sync that will never finish on a large store.

Bulk operations, and when to reach for them

Any job whose shape is "every product" or "every order since the beginning" should not be a paginated read at all. Bulk operations exist precisely for that: you submit the query once, Shopify runs it asynchronously against the whole dataset, and you collect a JSONL file of the results when it finishes. They do not carry the per-query cost cap or the rate limits that single queries do.

The trade is that they are asynchronous. You submit, you poll for status, you download a file, and you parse it line by line — nested connections come back as separate lines linked by a parent id, which catches everyone the first time. That is more moving parts than a loop, and it is the right answer anyway for a nightly catalogue sync, a first-time import, or an export to a warehouse system.

The rule we use: if the job is bounded and small — this order, this customer, the last hour of changes — page it. If the job is "everything", make it a bulk operation. A paginated full-catalogue sync on a 200,000-SKU store is a nightly incident waiting to happen.

The 1,000-point ceiling, and what it really means

A single query cannot cost more than 1,000 points, on any plan, and that is enforced before the query runs based on requested cost. Merchants on Plus sometimes read this as a limit their plan should have removed. It is not that kind of limit — it is a cap on the complexity of one request, not on your throughput. Plus raises how fast you can send queries, not how large one may be.

When you hit it, the answer is decomposition: split one deeply nested query into two shallower ones, or drop a nested connection and fetch those records separately. If a query is over the cap, it is usually because someone nested three connections to avoid a second round trip, which is a false economy.

Handle the throttle properly when you do hit it

A throttled request comes back as an error, not an HTTP failure you can blindly retry. Retrying immediately makes it worse, and retrying in a tight loop is how an integration gets itself into trouble.

Back off using the numbers the API gave you, not a fixed delay, and make the retry idempotent — mutations that are not safe to replay need an idempotency strategy of their own, because at some point one will be sent twice. That is the same failure class as webhook redelivery, and it is worth designing for once rather than discovering in production.

Questions this raises

How do you query the Shopify Admin GraphQL API efficiently?

Request only the fields you need, keep page sizes modest and page with cursors, read `extensions.cost.throttleStatus` to pace requests against the real remaining budget, and move any full-catalogue read or write to a bulk operation. Cost is calculated from the shape of the query before execution, so asking for fewer nodes is the direct fix for throttling.

What are the Admin GraphQL API rate limits?

The API uses a leaky bucket measured in cost points. The bucket refills continuously at 100 points per second on standard plans, 200 on Advanced Shopify, 1,000 on Shopify Plus and 2,000 on enterprise. A single query is capped at 1,000 points regardless of plan.

Why is my query throttled when the response is small?

Because you are charged for what you requested, not what came back. `first: 250` on a connection is priced as 250 nodes even if two match. Compare `requestedQueryCost` with `actualQueryCost` — a large gap means you are reserving capacity you never use.

When should you use a bulk operation instead of pagination?

When the job is unbounded — every product, every order, a full export. Bulk operations run asynchronously against the whole dataset and are not subject to the per-query cost cap or the rate limits. Keep pagination for bounded jobs like a single order or the last hour of changes.

Does Shopify Plus remove the query cost limit?

No. Plus raises the restore rate to 1,000 points per second, so you can send more queries per second. The 1,000-point ceiling on a single query applies on every plan, and hitting it means the query needs decomposing rather than a plan upgrade.

Should you hardcode a delay between API calls?

No. Read `throttleStatus` and wait (cost needed − currently available) ÷ restore rate. A hardcoded delay is either too slow, or too fast the moment the merchant changes plan.

NEXT STEP

Free store audit

A senior Shopify engineer reviews your storefront, theme performance and checkout, then sends a prioritised list of fixes.