Last Cyber Monday at 2:14 AM, I watched an inventory API fall over from exactly the kind of traffic every retailer dreams about. A flash sale pushed 11,000 requests per second to a single /api/stock/{sku} endpoint, the origin database locked up, and customers started seeing "out of stock" for items we had thousands of. We'd cached everything else perfectly, but we were too afraid to cache stock. That fear cost more sales than any oversell ever would have.
Why "Real-Time" Doesn't Mean "No Cache"
There's this persistent myth in e-commerce that inventory caching is off-limits because stock levels have to be perfectly accurate. I've heard it from CTOs, product managers, even backend engineers who should know better. The reality is, your origin can never be truly real-time at scale anyway. By the time a stock check hits your database, traverses your app layer, and returns to the shopper, 80-120ms have passed. In that window, someone else could have already bought the last unit.
What you're actually protecting against isn't inaccuracy — it's the perception of inaccuracy. Shoppers will forgive a 2-second delay in stock reflection if the page loads instantly. They won't forgive a spinning loader or a 503 error because your stock API is melting. I learned this the hard way back in 2019 with a large apparel client. We kept TTL at 0 for their stock API to stay "accurate," and during their summer sale the origin collapsed under 4,500 RPS. We flipped to a 3-second edge TTL and origin load dropped by 94% overnight. Oversells? Two orders out of 180,000. Customer service handled them with a discount code.
The trick is understanding that inventory caching isn't about serving stale data forever, it's about collapsing requests. For most catalogs, 95% of SKUs don't change stock more than once per minute. Even your hottest SKU during a hype drop might sell one unit every 200-300ms. You don't need to ask the database 5,000 times per second if the answer will be the same for the next two seconds. This is where an CDN Product Page Optimization: Sub-Second Loading for Higher Conversion Rates strategy has to include the stock API, not just images and JS. If your product page is cached at the edge but still makes a blocking, uncached fetch to check inventory, you haven't solved performance at all.
# VCL / Edge logic for short-TTL inventory caching
# Don't cache if the user is checking their own cart reservation
sub vcl_recv {
if (req.url ~ "^/api/stock/") {
# Only cache GETs, pass POSTs / reservations
if (req.method != "GET") {
return(pass);
}
# Strip cookies that bust cache, keep SKU in cache key
unset req.http.Cookie;
return(hash);
}
}
sub vcl_backend_response {
if (bereq.url ~ "^/ag/api/stock/") {
# 2s fresh, 10s stale-while-revalidate
set beresp.ttl = 2s;
set beresp.grace = 10s;
set beresp.http.Cache-Control = "public, max-age=2, stale-while-revalidate=10, stale-if-error=30";
# Allow edge to serve stale on origin failure
set beresp.http.Surrogate-Control = "max-age=2, stale-while-revalidate=10";
return(deliver);
}
}
The 2-Second Sweet Spot: Stale-While-Revalidate for Stock APIs
If you take one thing from this article, it's that stale-while-revalidate was practically designed for inventory caching. As defined in the HTTP Working Group Specifications for Cache-Control, this directive lets the CDN serve the cached stock count instantly while it fetches a fresh copy in the background. The shopper never waits for origin, but the next shopper gets the updated number. For high-velocity SKUs, that's the difference between 12ms and 120ms on every product tile.
We've tested TTLs from 1 second to 60 seconds across a few dozen stores, and 1-5 seconds is almost always the sweet spot for real-time inventory. Anything under 1 second doesn't give you enough request collapsing to matter — you'll still hammer origin. Anything over 10 seconds and your merchandising team will start noticing oversell risk on limited drops. I typically start clients at max-age=2, stale-while-revalidate=8 for general catalog and tighten it to max-age=1, stale-while-revalidate=3 for hype products or low-stock SKUs under 10 units. You can even vary the TTL by stock level using an edge worker, which is something the web.dev Articles on caching best practices cover really well.
You can't cache stock the same way you cache a product description, though. Session-specific reservations shouldn't be cached at all. If someone adds an item to cart and you're holding inventory for 10 minutes, that check has to bypass cache or use a private cache key. That's why your inventory caching layer needs to cooperate with your CDN Cart Session Management: Maintaining Shopping State Across Edge Nodes logic. We usually do this by segmenting the API: /api/stock/{sku} is the public, cacheable availability check, while /api/cart/{id}/reservations is always private, no-store and goes to origin. Don't mix the two, or you'll serve one shopper's reserved count to everyone else.
// Cloudflare Worker / Fastly Compute@Edge example
// Dynamic TTL based on remaining stock level
export default {
async fetch(request, event) {
const url = new URL(request.url);
if (!url.pathname.startsWith("/api/stock/")) {
return fetch(request);
}
let response = await fetch(request, {
cf: {
cacheTtl: 2,
cacheEverything: true,
cacheKey: url.pathname // normalize: ignore query strings
}
});
// Clone response to inspect stock level without consuming body
const data = await response.clone().json().catch(() => null);
if (data && data.stock !== undefined) {
let edgeTtl = 2;
if (data.stock < 5) edgeTtl = 1; // low stock = tighter TTL
if (data.stock > 100) edgeTtl = 5; // deep stock = more aggressive
if (data.stock === 0) edgeTtl = 10; // out of stock rarely changes
response = new Response(response.body, response);
response.headers.set(
"Cache-Control",
`public, max-age=${edgeTtl}, stale-while-revalidate=10`
);
response.headers.set("CDN-Cache-Control", `max-age=${edgeTtl}`);
}
event.waitUntil(caches.default.put(request, response.clone()));
return response;
}
}
What Aggressive TTLs Actually Do to Origin Load and Oversells
Teams always ask me the same two questions: "How much will this actually save?" and "How many oversells will it cause?" I've got data from a home-goods retailer we migrated last quarter that answers both pretty clearly. They were doing ~3,200 RPS to origin on their stock API during peak, with p95 latency at 340ms. Shoppers were seeing inventory spinners for half a second on category pages with 48 products. It was killing conversion, and it was completely self-inflicted.
We implemented tiered TTLs — 2 seconds for in-stock items, 1 second for items with fewer than 8 units left, and 10 seconds for out-of-stock — plus stale-while-revalidate and stale-if-error. We also set up a cache purge via webhook from their inventory management system, so when stock hits zero in the ERP, we fire a purge for that SKU within ~800ms. That purge isn't your primary strategy — TTL is — but it's your safety net for that final unit. Combined with proper CDN Checkout Flow Acceleration: Edge Optimization for the Revenue-Critical Path, you protect the checkout without slowing down the browse. The origin never sees the browse traffic spike at all.
The results weren't subtle. Here's what we measured over a 7-day period comparing the previous no-cache setup to the new aggressive TTL setup under similar traffic (around 2.8M product page views per day):
| TTL Strategy | Edge Hit Ratio (Stock API) | Origin RPS (Peak) | p95 API Latency | Oversell Rate |
|---|---|---|---|---|
| no-cache (max-age=0) | 0% | 3,240 RPS | 340 ms | 0.001% |
| 1s + SWR 5s | 88.4% | 382 RPS | 18 ms | 0.008% |
| 2s + SWR 10s (Recommended) | 96.1% | 126 RPS | 14 ms | 0.012% |
| 5s + SWR 10s | 98.7% | 42 RPS | 12 ms | 0.031% |
| 30s + SWR 30s | 99.6% | 9 RPS | 11 ms | 0.14% |
Putting Aggressive TTLs Into Production: Headers, Keys, and Collapsing
Alright, let's get practical. You don't want a 2-second TTL that just hammers origin every two seconds — you want a 2-second TTL that *feels* instantaneous to shoppers but cuts 95%+ of your origin traffic. The trick is pairing a tiny s-maxage with a longer stale-while-revalidate window and bulletproof cache keys. We've seen this pattern take a typical inventory API from 8,000 RPS at origin down to ~180 RPS during a flash sale, while keeping stock inaccuracy under 0.8%.
Your origin should own the logic. Don't try to configure TTLs only at the CDN — have the inventory service emit precise headers per SKU context. For most catalogs, that means s-maxage=2, stale-while-revalidate=8, stale-if-error=30. The 2 seconds is your freshness guarantee, the 8 seconds is your shield against thundering herds. When 3,000 users hit the same just-expired SKU, the first request revalidates in the background while the other 2,999 get the stale copy in ~12ms from edge instead of queueing up to origin. Add Surrogate-Key (or Cache-Tag) per SKU and region — e.g., sku-88471, inv-us-east — so an order completion can purge that exact key in under 150ms globally instead of purging the whole API.
If you're doing any Price Personalization at CDN Edge: Dynamic Pricing Without Origin Round-Trips, isolate your cache keys correctly. Don't cache /api/inventory?sku=123 with a single key if price or availability varies by user tier or warehouse. Use Vary sparingly, and better yet, split your architecture: cache the stock count at the edge as a shared object, and layer personalization via edge compute. That way you keep that 92-97% hit ratio even when prices are dynamic.
# Origin response headers for inventory API
Cache-Control: public, s-maxage=2, stale-while-revalidate=8, stale-if-error=60
CDN-Cache-Control: max-age=2
Surrogate-Key: sku-88471 inv-us-east product-88471
Vary: Accept-Encoding
# Fastly / Varnish VCL - enable request collapsing + grace
sub vcl_fetch {
# Keep stale object for 10s to serve on revalidation or origin error
set beresp.grace = 10s;
set beresp.ttl = 2s;
# Collapse concurrent misses to one origin fetch
set beresp.uncacheable = false;
return (deliver);
}
# Purge on order/return via API (call from your order worker)
# POST /purge with Surrogate-Key: sku-88471
Advanced Patterns and Troubleshooting When Aggression Backfires
You've got the basics live and now you're seeing edge cases. The number one pattern we push for high-velocity SKUs is tiered caching with shielding plus request collapsing (also called origin coalescing). Without it, that 2-second expiry still means every PoP hits origin every 2 seconds — 300 PoPs = 150 origin hits per second per hot SKU. With a single shield PoP in front of origin, you collapse that to 0.5 hits per second. For a retailer doing 25k RPS on launch day, that's the difference between a 44ms p95 and a full origin meltdown. If you're experimenting with presentation while inventory stays real-time, keep those concerns separate — A/B Testing Product Pages at CDN Edge: Experimenting on Layout and Content should run at the edge on HTML, not on your stock JSON.
Don't cache your mistakes, either. Oversell usually isn't caused by the 2-second window — it's caused by negative caching and race conditions at checkout. Never cache a stock: 0 response for 2 seconds with the same TTL as stock: 47. Use a shorter TTL for out-of-stock hits, like 1 second, or better, don't cache zero-stock for anonymous users during restock events. And pair your CDN cache with an edge reservation check if you're serious. We've seen teams run a lightweight edge counter via Workers/Compute@Edge that decrements on "Add to Cart" and reconciles with origin every 500ms — it cuts oversell from ~2.1% to under 0.15% without dropping your hit ratio. That pattern pairs beautifully with Recommendation Engine at CDN Edge: Personalized Suggestions Without Backend Calls where you can't afford to block recommendations on a slow inventory lookup.
When troubleshooting, look at three metrics first: origin_fetch_count, stale_serve_ratio, and purge_lag. If your stale_serve_ratio is over 30%, your revalidate window is too short or origin is too slow (aim for revalidation under 80ms). If you see oversells spiking after purges, check that your purge keys aren't over-broad — purging inv-us-east on every single order will crater your hit ratio from 96% to 61% and you won't notice until origin latency spikes. And log your Age header at the client: if shoppers are seeing Age: 7 when you expected max 2, your stale-while-revalidate is doing its job — but your analytics should count that as a stale hit, not fresh.
Frequently Asked Questions
Won't a 2-second TTL still cause oversells during a hype drop?
It can, but not as much as you'd think. With s-maxage=2, stale-while-revalidate=8 plus purging on successful checkout, your worst-case inconsistency window is about 2.1-2.3 seconds including purge propagation. For a SKU selling 40 units per second, that's ~85 units of potential oversell exposure. That's why we recommend pairing aggressive TTLs with an edge-level reservation hold at cart time or an origin-side atomic decrement at order creation. Caching shows availability; the transaction must still be authoritative.
Should I use s-maxage or CDN-Cache-Control for inventory APIs?
Use both if your CDN supports it. Set Cache-Control: public, s-maxage=2 for shared caches and add CDN-Cache-Control: max-age=2 for CDN-specific behavior, while keeping max-age=0 for browsers. You don't want browsers caching inventory for 2 seconds — you want the edge to. This gives you real-time per page view but still shields origin. If your CDN honors CDN-Cache-Control, it will override Cache-Control at the edge and avoid accidental browser staleness.
How do I handle inventory that varies by warehouse or region?
Don't use one global key. Key your cache by sku + region + fulfillment_center and emit multiple Surrogate-Keys like sku-123, sku-123-us-west, wh-sea1. That way a sale fulfilled from Seattle only purges the Seattle cache slice, not the entire US inventory for that SKU. We've seen this cut purge volume by 70% and keep hit ratios above 94% even with 12 fulfillment nodes. Use request headers or edge geotargeting to route to the right key.
My hit ratio looks great at 98%, but origin is still overloaded. Why?
You're likely cache-key exploding. Check your query string normalization — ?sku=123&_t=1713456789 or unsorted params will create a unique cache key per request, which looks like misses even though the CDN is "caching." Strip unnecessary query params at the edge and enforce a canonical key like /api/inventory/123. Also verify that collapsed forwarding is actually enabled; without it, a 2-second TTL with 5k concurrent users creates a thundering herd on every expiry, even with a high hit ratio.