GET A FREE STORE AUDITFREE AUDIT

SEP 18, 2026 · UPDATED OCT 8, 2026 · 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.

Two white mugs labeled REQUEST and RESPONSE joined by a twisted string carrying small tags reading MESSAGE and REPLY

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 catalog 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 at 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 (your submit and status requests still count), and are the right tool for any full-catalog read or write.

Why is my query throttled when the response is small?

Because the Admin GraphQL API charges you for what you asked for, before it knows what exists, and that surprises people coming from REST. Requesting first: 250 on a connection is priced as 250 nodes whether the store has 250 matching orders or none. Shopify documents this in its GraphQL Admin rate limits: the bucket must cover the requested cost before execution begins. 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 catalog that is being edited while you read it silently skips and duplicates records.

Two practical rules. First, pick a page size deliberately: the largest page you can request 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.

Offset pagination over a catalog that is being edited while you read it silently skips and duplicates records.
Mitesh Patel & Krunal Prajapati

When should you use a bulk operation instead of pagination?

When the job is "every product" or "every order since the beginning": that should not be a paginated read at all. You submit the query once with bulkOperationRunQuery, Shopify runs it asynchronously, and you collect a JSONL file of the results when it finishes. Per Shopify, bulk operations do not have the max cost limit or rate limits that single queries have. Your own submit and status requests still count as normal API requests.

The trade is that they are asynchronous. You submit, you wait for completion, you download a file, and you parse it line by line. Shopify recommends subscribing to the bulk_operations/finish webhook over polling, though delivery is not guaranteed, so keep a status poll as a backstop. Nested connections come back as separate lines after their parent, each carrying a __parentId, which catches everyone the first time. The query itself is limited to five connections and two levels of nesting, and the download URL expires after a week. That is more moving parts than a loop, and it is the right answer anyway for a nightly catalog 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-catalog 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

Retrying immediately makes a throttle worse, and retrying in a tight loop is how an integration gets itself into trouble. Stop sending until the bucket has capacity again.

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. If you are building this layer for a client, it is the core of our custom Shopify apps and back ends work.

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

General information, not legal, tax or financial advice. Rates, limits and deadlines come from the vendors and authorities named, and they change: check the linked source before relying on one.

PART OF OUR SERVICE

Wire Shopify into the systems your business already runs on

ERP, subscriptions, custom apps and configurators

EXPLORE INTEGRATE →

NEXT STEP

Free store audit

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

A hand gestures towards a small shop doorway tied with colorful ribbons, with a tablet on a workbench inside