The Shopify Admin API: querying, pagination and rate limits
GraphQL, cursor pagination and a rate limit based on query cost. Get these three right and everything else follows.
9 min read · Apps & checkout ·
The Admin API is how an app reads and changes a store's data: products, orders, customers, inventory, fulfilment, metafields, discounts. It's GraphQL, it's versioned quarterly, and it's rate-limited by query cost rather than request count. Those three facts shape almost every architectural decision you'll make.
Queries and mutations
``graphql query { products(first: 50, query: "status:active") { edges { cursor node { id title totalInventory } } pageInfo { hasNextPage endCursor } } } ``
``graphql mutation { productUpdate(input: { id: "gid://shopify/Product/123", title: "New title" }) { product { id title } userErrors { field message } } } ``
Two things to internalise immediately:
Always select userErrors. A mutation can return HTTP 200 with the operation having failed. Code that only checks the status code will silently do nothing and report success — the single most common bug in first Shopify integrations.
IDs are global IDs (gid://shopify/Product/123), not bare numbers. If you're storing Shopify IDs in another system, store the whole GID.
Pagination
There's no page number. You get a cursor, and you ask for what comes after it:
`` products(first: 250, after: "eyJsYXN0X2lkIjo...") ``
Loop while pageInfo.hasNextPage, passing endCursor each time. 250 is the maximum page size.
Code written against a development store with twelve products will pass tests and fail in production. Write the pagination loop from the first query, not when it breaks.
Rate limits
The Admin GraphQL API uses a calculated query cost model with a leaky bucket. Every query has a cost based on how much data it requests; you have a bucket of points that refills at a fixed rate. Ask for more, and you wait.
Every response tells you where you stand:
``json "extensions": { "cost": { "requestedQueryCost": 102, "actualQueryCost": 46, "throttleStatus": { "maximumAvailable": 2000, "currentlyAvailable": 1954, "restoreRate": 100 } }} ``
Three practical consequences:
- Ask only for the fields you need. Cost scales with what you request, so a query selecting twenty fields you ignore costs you real throughput.
- Read
throttleStatusand pace yourself. Don't fire requests until you get throttled and then back off — watch the bucket and stay inside it. - Handle throttling anyway. Retry with exponential backoff. A naive full-catalogue push at midnight hitting the ceiling and half-failing quietly is exactly the failure described in ERP integration.
Bulk operations
For anything involving the whole catalogue, don't paginate — use bulk operations. You submit a query, Shopify runs it asynchronously, and you download a JSONL file of results when it's done.
``graphql mutation { bulkOperationRunQuery(query: "{ products { edges { node { id title } } } }") { bulkOperation { id status } userErrors { field message } } } ``
Bulk mutations work the same way in reverse: upload a JSONL file of inputs, Shopify processes them.
Rule of thumb: more than a few thousand records, use bulk. It's exempt from the normal cost limit and it's the difference between a sync that takes four minutes and one that takes four hours.
Versioning
API versions are released quarterly and supported for a year. You pin a version in your requests. When it's retired, unpinned or stale code breaks.
Two habits that make this a non-event:
- Pin explicitly and store the version in configuration, not scattered through the code.
- Read the changelog each quarter and diary an upgrade. An hour every three months beats an emergency.
This maintenance is a real ongoing cost and belongs in any app budget.
REST or GraphQL
The full comparison is in GraphQL vs REST.
REST still exists. New development should be GraphQL: new capability lands there first, some features exist only there, and the cost model gives you more control than REST's per-request limit.
Reading data you didn't cause
Webhooks tell you when something changes, but delivery is at-least-once and occasionally none-at-all. Any system that must not miss a record needs a reconciliation pass — a periodic query that catches what webhooks dropped. Webhooks covers the pattern.
Practical checklist
- Select
userErrorson every mutation and act on it. - Write pagination on the first query, not the first outage.
- Read
throttleStatus; pace, then retry with backoff. - Use bulk operations above a few thousand records.
- Pin the API version and diary the upgrade.
- Log every request and response for anything touching orders.
- Store global IDs, not numeric ones.
Nearly every failed Shopify integration I've been called into failed on one of three things: pagination, rate limits, or a mutation that returned 200 and did nothing.
Is this the problem you’re looking at?
Send me the link to your store and a line about what is going wrong. You get a straight answer within one business day — no pitch, no obligation.
[email protected]Or see what I do around Shopify: services, work beyond the theme, selected work.