If you have ever watched a Shopify integration grind to a halt during a flash sale or a bulk catalog update, you already know the pain of hitting an API ceiling. The Shopify GraphQL Admin API does not count requests the way REST does. It counts cost. That single difference reshapes how you design clients, batch work, and recover from throttling.
This article breaks down how Shopify GraphQL rate limits actually work, why the calculated query cost model exists, and how to engineer systems that stay fast without tripping the limiter. We will cover the leaky bucket mechanics, cost estimation, retry logic, and architectural patterns that keep throughput high under pressure.
Why Shopify GraphQL Rate Limits Work Differently
REST APIs on Shopify use a simple request-based limit. You get a bucket of calls, you spend them, and you wait for a refill. GraphQL throws that model out.
With GraphQL, a single query can ask for one field or ten thousand nested records. Counting requests would be meaningless. A naive query that fetches 250 products with variants, images, and metafields costs far more than a query that fetches a single order. So Shopify charges you based on the complexity and volume of what you request.
This is the foundation of rate limit management GraphQL engineers need to internalize. You are not spending requests. You are spending points.
If you are moving an older codebase, understanding this shift is central to any REST to GraphQL migration. The cost model changes how you plan every call.
The Calculated Query Cost Model
Every GraphQL query gets a cost before and after it runs. Shopify returns this in the extensions.cost object on each response.
There are two numbers that matter:
- Requested query cost: the estimated cost calculated before execution.
- Actual query cost: the real cost after execution, which is often lower because pagination may return fewer items than requested.
Here is a sample of what Shopify sends back.
{
"extensions": {
"cost": {
"requestedQueryCost": 102,
"actualQueryCost": 42,
"throttleStatus": {
"maximumAvailable": 2000,
"currentlyAvailable": 1958,
"restoreRate": 100
}
}
}
}
The throttleStatus block is the heart of Shopify API throttling. It tells you how much you have, how much you had, and how fast it comes back.
How Cost Is Calculated
Shopify assigns point values to different operations. The general rules are straightforward.
| Operation type | Cost |
|---|---|
| Scalar or object field | 0 points |
| Connection (list) field | 2 points plus child costs |
| Mutation | 10 points |
| Each returned node in a connection | Based on requested count |
Connections are where costs balloon. When you request first: 250 on a products connection and then nest variants and metafields inside, the requested cost multiplies quickly. Learning to trim these queries is the core of query cost optimization.
The Leaky Bucket Algorithm
Shopify uses a leaky bucket model to manage your Shopify API quota. Picture a bucket with a fixed capacity that leaks at a steady rate.
- The bucket capacity is
maximumAvailable, usually 2000 points for standard plans. - The leak rate is
restoreRate, usually 100 points per second. - Every query drains points equal to its actual cost.
As long as the bucket does not overflow, your queries run. When a query would push the requested cost above what is currently available, Shopify rejects it with a throttled error.
Plan-Based Limits
Your available capacity scales with your Shopify plan.
| Plan | Bucket size | Restore rate |
|---|---|---|
| Standard | 2,000 points | 100 points/sec |
| Advanced | 4,000 points | 200 points/sec |
| Plus | 20,000 points | 1,000 points/sec |
Plus merchants get a far larger ceiling, which matters for high-volume operations like large product catalog synchronization or multi-warehouse systems.
Reading Throttle Status in Real Time
The smartest clients do not guess. They read the throttleStatus after every call and adjust pace accordingly. This is the difference between a system that constantly retries and one that glides just under the limit.
Here is a basic pattern for tracking available points in a client.
function updateBudget(response) {
const cost = response.extensions.cost;
const status = cost.throttleStatus;
return {
available: status.currentlyAvailable,
max: status.maximumAvailable,
restoreRate: status.restoreRate,
lastCost: cost.actualQueryCost
};
}
function canAfford(budget, estimatedCost) {
return budget.available >= estimatedCost;
}
If you cannot afford the next query, you wait. The wait time is simple math.
function waitTimeMs(budget, estimatedCost) {
const deficit = estimatedCost - budget.available;
if (deficit <= 0) return 0;
return Math.ceil(deficit / budget.restoreRate) * 1000;
}
This proactive throttling is a core trait of any high-performance Shopify API client. You stop sending requests that you know will fail.
Handling Throttled Responses
Even with careful budgeting, you will hit limits during bursts. Shopify returns a specific error when you do.
{
"errors": [
{
"message": "Throttled",
"extensions": {
"code": "THROTTLED"
}
}
]
}
When this happens, do not retry immediately. Use exponential backoff with jitter. Blind retries create a thundering herd that keeps the bucket empty.
async function withBackoff(fn, maxRetries = 5) {
let attempt = 0;
while (attempt < maxRetries) {
try {
return await fn();
} catch (err) {
if (err.code !== "THROTTLED") throw err;
const delay = Math.min(1000 * 2 ** attempt, 16000);
const jitter = Math.random() * 500;
await sleep(delay + jitter);
attempt++;
}
}
throw new Error("Max retries exceeded");
}
For a deeper treatment of recovery sequencing, review these rate limit recovery strategies and the broader set of error recovery patterns that keep failures contained.
Estimating Cost Before You Send
The best engineers calculate cost before sending a query. This lets you throttle on your side and avoid rejections entirely.
A rough estimate follows the connection math. For each connection, multiply the requested count by the cost of a single node, then add connection overhead.
| Query element | Estimated contribution |
|---|---|
| products(first: 50) | 2 + (50 x node cost) |
| variants(first: 10) per product | 2 + (10 x node cost) |
| scalar fields | 0 |
| one mutation | 10 |
By precomputing this, you build a budget-aware scheduler. Pair this with parallel query optimization and you can run concurrent requests that collectively stay under the bucket ceiling.
Pagination and Cost Control
Pagination is where cost discipline pays off. Fetching 250 items at once costs more per call but fewer calls overall. Fetching 10 at a time costs less per call but may burn more round trips.
The sweet spot depends on your node complexity. Heavy nodes with many nested fields favor smaller page sizes. Lean nodes favor larger pages.
Cursor-based pagination is the only supported approach in GraphQL, and tuning it well is central to pagination performance. Always request only the fields you need. Every extra nested connection raises the per-node cost.
query GetProducts($cursor: String) {
products(first: 100, after: $cursor) {
edges {
cursor
node {
id
title
}
}
pageInfo {
hasNextPage
}
}
}
Notice how lean that query is. No variants, no metafields, no images. Each addition would multiply the cost across 100 nodes.
Bulk Operations for Large Jobs
When you need to export or import huge datasets, point-based throttling becomes a bottleneck. This is exactly the scenario the Bulk Operations API was built for.
Bulk operations run asynchronously on Shopify’s side. You submit a query, Shopify processes it in the background, and you poll for a result file. A single bulk operation does not drain your point bucket the way thousands of paginated calls would.
Use bulk operations when:
- You need every product, order, or customer record.
- The job can tolerate minutes of latency.
- You want to avoid consuming your live quota for background work.
This pattern fits neatly into async Shopify architectures where heavy jobs run off the critical path and never block interactive traffic.
Caching to Reduce API Pressure
The cheapest query is the one you never send. Caching is the most underused rate limit tool.
If product data changes infrequently, cache it. If a customer record was fetched five seconds ago, reuse it. A well-placed cache layer can cut your point consumption dramatically and is often the fastest win for reducing API consumption costs.
Study practical caching strategies for GraphQL and the broader design of caching layers across Shopify ecosystems. Combine them with webhooks so your cache invalidates on real changes rather than on a timer.
Architectural Patterns for Rate Limit Safety
Rate limit engineering is not only about individual queries. It is about system design. Several patterns keep large systems stable under throttling pressure.
Circuit Breakers
When Shopify starts throttling heavily, hammering the API makes things worse. A circuit breaker trips after repeated failures, pauses outbound traffic, and gives the bucket time to refill. This is a proven circuit breaker pattern for Shopify APIs.
Backpressure
If your job queue fills faster than Shopify can serve, you need a way to slow intake. Proper backpressure handling prevents your own system from generating requests it cannot send.
Queues and Workers
Route all Shopify calls through a centralized queue. A fixed pool of workers pulls from it at a controlled rate. This gives you a single choke point to enforce your quota. It pairs naturally with async event processing and load balancing across Shopify systems.
Idempotency
When you retry a throttled mutation, you risk duplicate writes. Strong idempotency strategies ensure a retried order creation does not produce two orders. This also helps prevent race conditions in order processing.
A Practical Budget-Aware Client
Putting the pieces together, a robust client looks like this.
class ShopifyRateLimiter {
constructor() {
this.available = 2000;
this.restoreRate = 100;
this.lastUpdate = Date.now();
}
refill() {
const elapsed = (Date.now() - this.lastUpdate) / 1000;
this.available = Math.min(2000, this.available + elapsed * this.restoreRate);
this.lastUpdate = Date.now();
}
async acquire(cost) {
this.refill();
while (this.available < cost) {
const wait = ((cost - this.available) / this.restoreRate) * 1000;
await sleep(wait);
this.refill();
}
this.available -= cost;
}
sync(throttleStatus) {
this.available = throttleStatus.currentlyAvailable;
this.restoreRate = throttleStatus.restoreRate;
this.lastUpdate = Date.now();
}
}
The acquire method blocks until enough points exist. The sync method corrects your local estimate using the real throttleStatus from each response. This hybrid of local prediction and server truth is what keeps throughput smooth.
Monitoring and Testing
You cannot manage what you do not measure. Log the actualQueryCost and currentlyAvailable for every call. Chart them over time. Spikes tell you where your queries are heavy.
Set alerts when available points dip below a threshold. Integrate this into your incident response process so throttling events trigger a known runbook instead of panic.
Before shipping, run reliability testing that simulates burst traffic. Confirm your backoff, circuit breaker, and queue behave correctly under sustained load. This matters especially for integrations like CRM synchronization, WMS integration, and enterprise order routing where downstream systems depend on steady Shopify throughput.
Common Mistakes to Avoid
A few recurring errors cause most throttling incidents.
| Mistake | Better approach |
|---|---|
| Retrying instantly on THROTTLED | Exponential backoff with jitter |
| Requesting first: 250 with deep nesting | Reduce page size or split queries |
| Ignoring throttleStatus | Sync budget after every response |
| Running bulk exports as paginated loops | Use the Bulk Operations API |
| No caching layer | Cache stable data and invalidate via webhooks |
Each of these is a quick fix with outsized impact on your Shopify API quota health.
Bringing It Together
Shopify GraphQL rate limits reward engineers who think in points, not requests. Read the cost model, respect the leaky bucket, and build a client that budgets proactively. Layer in caching, bulk operations, and circuit breakers, and your system will stay fast even when traffic spikes.
The teams that win are the ones that treat rate limiting as a design constraint from day one, not a bug they patch later. Build it into your scheduler, your queue, and your retry logic. Your integration will be faster, cheaper, and far more reliable as a result.
Frequently Asked Questions
What is the default Shopify GraphQL rate limit?
Standard plans get a 2,000 point bucket that refills at 100 points per second. Advanced and Plus plans get larger buckets and faster restore rates.
How is GraphQL query cost calculated on Shopify?
Scalar and object fields cost 0 points, connections cost 2 points plus the cost of returned nodes, and mutations cost 10 points. Shopify returns both requested and actual cost in the response extensions.
What happens when I exceed the rate limit?
Shopify returns a THROTTLED error with no data. You should back off, wait for the bucket to refill, and retry with exponential backoff and jitter.
How do I avoid hitting Shopify API throttling?
Read the throttleStatus after each call, estimate cost before sending, cache stable data, and use the Bulk Operations API for large jobs instead of paginated loops.
Does the Bulk Operations API count against my rate limit?
Bulk operations run asynchronously and do not drain your point bucket the way thousands of paginated queries would, making them ideal for large exports and imports.
What is the difference between requested and actual query cost?
Requested cost is the estimate before execution based on the maximum possible nodes. Actual cost reflects what the query truly returned, which is often lower when connections return fewer items.
