Back to Blog
Shopify Development

Shopify GraphQL Throttled Error: How to Fix It

Alex SterlingOctober 13, 20265 min read
Shopify GraphQL Throttled Error: How to Fix It

Your Shopify integration is returning THROTTLED and jobs are failing halfway through.

The Admin GraphQL API does not limit you by request count. It limits by query cost, which is why a script making only a few calls a minute can still get throttled.

Symptom

{
  "errors": [{
    "message": "Throttled",
    "extensions": { "code": "THROTTLED" }
  }],
  "extensions": {
    "cost": {
      "requestedQueryCost": 2004,
      "actualQueryCost": null,
      "throttleStatus": {
        "maximumAvailable": 2000.0,
        "currentlyAvailable": 1847,
        "restoreRate": 100.0
      }
    }
  }
}

Note requestedQueryCost exceeding maximumAvailable: that request could never succeed, at any time, no matter how long you wait.

Cause

Shopify uses a leaky bucket:

  • A standard store has a bucket of 2,000 points, refilling at 100 points/second.
  • Every query costs points calculated from the fields requested, before it runs.
  • Connections cost proportionally to first/last — asking for 250 items costs far more than asking for 10.
  • If the cost exceeds what is currently available, you get THROTTLED.

Two failure modes look identical but need different fixes:

  1. Transient — the bucket is temporarily drained. Waiting fixes it.
  2. Structural — a single query costs more than the maximum bucket. Waiting never fixes it; the query must be smaller.

Fix

1. Read the cost, do not guess it

Every response includes an extensions.cost block. Log it while developing:

const json = await res.json();
const cost = json.extensions?.cost;
if (cost) {
  console.log(
    `cost ${cost.actualQueryCost}, ` +
    `available ${cost.throttleStatus.currentlyAvailable}/${cost.throttleStatus.maximumAvailable}`
  );
}

2. Retry using the real restore rate

Do not use a fixed delay. Compute how long the bucket actually needs:

async function shopifyGraphQL(query, variables, attempt = 1) {
  const res = await fetch(endpoint, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Shopify-Access-Token': token,
    },
    body: JSON.stringify({ query, variables }),
  });

  const json = await res.json();
  const throttled = json.errors?.some((e) => e.extensions?.code === 'THROTTLED');

  if (!throttled) return json;
  if (attempt > 5) throw new Error('Still throttled after 5 attempts');

  const status = json.extensions?.cost?.throttleStatus;
  const needed = json.extensions?.cost?.requestedQueryCost ?? 100;

  // A query costing more than the whole bucket can never succeed — fail loudly
  // rather than retrying forever.
  if (status && needed > status.maximumAvailable) {
    throw new Error(
      `Query costs ${needed}, bucket maximum is ${status.maximumAvailable}. Reduce page size.`
    );
  }

  const deficit = status ? needed - status.currentlyAvailable : 100;
  const waitMs = Math.max(1000, Math.ceil((deficit / (status?.restoreRate ?? 100)) * 1000));
  await new Promise((r) => setTimeout(r, waitMs));

  return shopifyGraphQL(query, variables, attempt + 1);
}

3. Page smaller, and only request what you use

The single biggest cost reduction is a smaller first:

# Expensive — nested connections multiply
query {
  products(first: 250) {
    edges { node {
      id title
      variants(first: 100) { edges { node { id price inventoryQuantity } } }
      metafields(first: 50) { edges { node { key value } } }
    } }
  }
}

# Much cheaper
query($cursor: String) {
  products(first: 50, after: $cursor) {
    edges { cursor node { id title } }
    pageInfo { hasNextPage }
  }
}

Fetch variants for the products you actually need, rather than eagerly for all of them.

4. Use bulk operations for large exports

Anything over a few thousand records should not be paginated at all:

mutation {
  bulkOperationRunQuery(
    query: """
      { products { edges { node { id title handle } } } }
    """
  ) {
    bulkOperation { id status }
    userErrors { field message }
  }
}

Shopify runs it server-side and hands back a JSONL file. It does not consume the bucket, and there is no rate limit to manage.

Verify

  1. Log actualQueryCost for each query and confirm your steady-state average is below the 100/second restore rate.
  2. Run a full sync and confirm zero unhandled THROTTLED errors.
  3. Deliberately trigger a throttle and confirm the retry recovers rather than crashing.

Prevent

  • Budget by cost, not request count. Sustained throughput is capped at ~100 points/second.
  • Never hard-code first: 250. Start at 50 and raise only with cost data in hand.
  • Use bulk operations for anything resembling an export.
  • Alert on repeated throttles — it usually signals a query that grew as the catalogue grew.

Frequently asked questions

Does Shopify Plus get a bigger bucket?

Yes. Plus stores get a larger maximum and faster restore rate. The mechanism is identical, so code written against the returned throttleStatus adapts automatically — another reason not to hard-code limits.

Is the Storefront API limited the same way?

No. The Storefront API is rate-limited per IP by request volume rather than calculated cost, so these patterns apply specifically to the Admin GraphQL API.

Why did this start failing only recently?

Almost always catalogue growth. A query over 50 products costs far less than the same query over 5,000, and nothing in your code changed.

Need a hand?

We build and maintain Shopify integrations for merchants in the USA, UAE, Saudi Arabia, Qatar and the UK. If your sync jobs keep failing, get a free audit or read about our Shopify development work.

Share
#Shopify GraphQL throttled#Shopify API rate limit#Shopify GraphQL query cost#Shopify Admin API 429#Shopify leaky bucket#Shopify API retry logic
Y

The Yaxify Team

Shopify & Web Development Agency

We build custom Shopify stores and high-performance websites for growing businesses across the USA, UK, and UAE. Everything we publish comes out of client work we have actually shipped.

More about Yaxify →

Ready to start your project?

Get a free consultation and custom quote within 24 hours

No commitment required • Response within 2 hours