Shopify Storefront API Rate Limits at Peak Traffic: How to Not Melt on Black Friday
Storefront API throttling is the silent killer of headless Shopify stores on peak days. Here's how to architect around it before your PDPs start returning empty JSON at 11:47pm on Cyber Monday.
Every year around mid-November, we get the same Slack message from a panicked founder: "Our headless storefront is returning empty product data intermittently, what do we do?" The answer is almost always the same — you're hitting Shopify Storefront API rate limits, and no amount of scaling your Vercel functions will fix it. The limit lives on Shopify's side.
This piece is the pre-mortem we wish every headless team ran in September, not the post-mortem they run in December.
How Storefront API Limits Actually Work
Shopify's Storefront API uses a calculated query cost model, not a simple requests-per-second counter. Every GraphQL query is assigned a cost based on the fields you request — connections and nested objects cost more, scalar fields cost less. You get a bucket that refills over time, and when the bucket empties, you get throttled.
The important nuance most teams miss: the bucket is scoped to the buyer IP for unauthenticated Storefront API calls, not to your app or your shop. That sounds generous until you realise what happens when your Next.js server does SSR — suddenly every request looks like it's coming from your origin server's IP, and you burn through one bucket for your entire storefront.
The three failure modes we see
- Server-side rendering collapse. Your ISR or SSR functions all egress from a handful of IPs. Under load, Shopify sees one hyperactive "buyer" and throttles the whole storefront.
- Client-side cart chatter. Every mini-cart update, every variant swap, every "you may also like" carousel fires its own query. On a slow 4G connection with retries enabled, one user can generate 30+ queries per session.
- Third-party sync jobs sharing the bucket. A reviews app or an inventory sync running against the same shop during peak hours will happily eat your headroom without telling you.
Measure Your Query Cost Before You Optimise Anything
Every Storefront API response includes an extensions.cost block. If you're not logging this, you're flying blind. Here's the shape:
{
"extensions": {
"cost": {
"requestedQueryCost": 152,
"actualQueryCost": 47,
"throttleStatus": {
"maximumAvailable": 1000,
"currentlyAvailable": 953,
"restoreRate": 50
}
}
}
}
The gap between requestedQueryCost and actualQueryCost is where you'll find easy wins. Requested cost is calculated on connection sizes you asked for (e.g. products(first: 250)), while actual cost reflects what was returned. If you're asking for 250 and getting 12, you're pre-paying for a bucket you don't use.
Wrap your fetch client so every response gets logged to your observability tool of choice. We usually pipe it to a dedicated shopify.cost metric in Datadog with tags for query name and route.
async function storefrontFetch(query: string, variables: object) {
const res = await fetch(STOREFRONT_URL, {
method: 'POST',
headers: {
'X-Shopify-Storefront-Access-Token': TOKEN,
'Content-Type': 'application/json',
},
body: JSON.stringify({ query, variables }),
});
const json = await res.json();
const cost = json.extensions?.cost;
if (cost) {
metrics.gauge('shopify.cost.actual', cost.actualQueryCost);
metrics.gauge('shopify.cost.available', cost.throttleStatus.currentlyAvailable);
if (cost.throttleStatus.currentlyAvailable < 200) {
logger.warn('Storefront bucket low', { available: cost.throttleStatus.currentlyAvailable });
}
}
return json;
}
Once you have this, run a load test in staging that mimics your expected Black Friday RPS. In our experience, teams are shocked to find their PDP query alone costs 80-150 points, and their homepage costs 300+.
Cache Aggressively, But Cache the Right Layer
Caching is the actual answer to rate limits. The question is where.
CDN edge cache is your first line
For anonymous traffic — which is 80-95% of storefront requests on most stores — you should be serving fully cached HTML from the edge. Vercel, Cloudflare, Netlify, Fastly, whichever. The trick is deciding your stale-while-revalidate window.
Our rule of thumb for peak season:
- Product pages: cache for 60s, SWR for 3600s. Inventory can be stale for a minute; the checkout is the source of truth.
- Collection pages: cache for 300s, SWR for 3600s. Sort order rarely changes hour-to-hour.
- Homepage: cache for 60s, SWR for 600s. Merchandising changes fastest here.
- Cart and checkout: never cache.
The SWR value matters more than the cache value during a spike. When your origin is throttled, SWR is what serves the slightly stale page instead of erroring.
Application-layer cache for shared queries
Behind your CDN, keep a small Redis or Upstash cache for queries that are identical across many pages — things like navigation menus, footer collections, currency conversion tables. These have no reason to be re-fetched per request.
One footgun: don't cache authenticated queries (customer data, cart contents) at this layer. It's the single fastest way to accidentally leak one customer's data to another.
Query Shape: The Cheapest Optimisation
Most headless codebases we audit have GraphQL queries copy-pasted from a tutorial two years ago. They over-fetch by 3-10x. Two rules go a long way:
- Only request
first: Nwhere N is what you'll actually render. If your PDP shows 4 related products, don't ask for 20. - Avoid nested connections where you can.
products.edges.node.variants.edges.node.metafieldsis a cost bomb. Split it into two queries and cache the metafields separately.
While you're in there, use persisted queries if your framework supports them. Hydrogen and the Shopify JavaScript client both support this. Persisted queries let Shopify recognise repeated query shapes and can reduce parsing overhead, though they don't reduce your query cost.
The Fallback Layer: What to Serve When Shopify Says No
Even with all the caching in the world, you need to plan for the moment when Storefront API returns a THROTTLED error mid-render. Your options, in increasing order of effort:
- Return the SWR copy. If your CDN has a stale version, serve it. Users won't notice.
- Return a static "safe" version of the page. Prebuilt at deploy time, stripped of dynamic data. Better than a 500.
- Queue non-critical requests. For things like recommendation carousels, degrade gracefully to nothing rather than blocking the render.
What you should absolutely not do is retry aggressively. We've seen teams add exponential backoff that starts at 100ms and retries 5 times — during a throttle event, that's just DDoSing yourself and Shopify.
A minimal retry policy that behaves
async function withThrottleHandling(fn: () => Promise<any>) {
try {
return await fn();
} catch (err) {
if (err.code === 'THROTTLED') {
// Wait one restore cycle, single retry only
await sleep(2000);
return await fn();
}
throw err;
}
}
One retry, one wait. If it fails again, fall back to cache or a safe response. Don't try to be clever.
Isolate Your Sync Jobs
If you have inventory syncs, product feed exporters, or reviews imports running against the same shop, move them to the Admin API where possible, and schedule them outside your peak traffic windows. The Admin API has its own leaky bucket that's independent of the Storefront API, so a well-behaved Admin API job won't eat your storefront's headroom.
For the ones that must run during peak — say, near-real-time inventory sync — put them behind a queue with strict concurrency limits. One worker, not ten. Peak day is not the time to catch up on your backlog.
What We'd Do This Week
If your Black Friday is looming and you're reading this in a mild panic:
- Day 1: Add cost logging to every Storefront API call. Ship it to production today. You need at least a week of baseline data.
- Day 2-3: Audit your top 5 queries by frequency. Cut over-fetching. Aim for 40-60% cost reduction on PDP and collection queries.
- Day 4: Review your CDN cache headers. If your product pages aren't hitting the edge for anonymous users, fix that before anything else.
- Day 5: Run a load test at 3-5x your expected peak RPS. Watch the
currentlyAvailablemetric. If it dips below 200, you've got work to do. - The week before peak: Freeze deploys. Freeze app installs. Freeze anything that touches the storefront.
If you'd rather have someone else do the audit, that's what our e-commerce engineering team exists for. But honestly, the logging step alone will tell you 80% of what you need to know — and you can do it yourself in an afternoon.
Want a team like ours?
72Technologies builds production software for the kind of teams who actually read this blog.
Start a projectKeep reading
Cart Abandonment Recovery on Shopify: What Actually Moves the Needle in 2026
Most cart abandonment recovery advice is stuck in 2019. Here's what we've seen actually recover revenue on Shopify stores in 2026 — and what's a waste of engineering time.
Shopify Collection Pages at 10,000 SKUs: Faceted Filtering Without Killing TTFB
Faceted filtering on large Shopify catalogs quietly destroys collection page performance. Here's how we architect it to keep TTFB under 400ms without a full replatform.
Shopify Functions vs Scripts: What Actually Runs at Checkout in 2026
Scripts are gone, Functions are the new contract. Here's what we learned porting discount logic, delivery customizations, and payment gating to Shopify Functions — and where the model still hurts.
