Every CDN cache has an expiration moment where fresh becomes stale. Traditionally, that moment triggers a synchronous origin fetch that blocks the client. With stale-while-revalidate and stale-if-error, you change that contract: the edge can serve stale immediately while refreshing in the background, or serve stale when the origin is failing. Implemented correctly, these two directives cut tail latency by 60-90% and turn origin outages from hard downtime into invisible blips.
This guide explains how both patterns work at the HTTP and edge level, when to use them, and how to configure them safely without amplifying cache invalidation errors or serving poisoned content.
Why Serving Stale Beats Serving Nothing
Without stale directives, a cache hit lifecycle is simple: HIT if age < max-age, otherwise MISS and block. That blocking revalidation creates two problems at scale. First, thundering herds: when a popular object expires, thousands of edge requests serialize behind one origin fetch, spiking TTFB and origin load. Second, fragility: any 5xx or timeout during revalidation becomes a user-visible error, even though a perfectly usable previous version sits on disk.
| Metric | Edge Processing | Origin Processing | Improvement |
|---|---|---|---|
| Latency (p50) | 8–15 ms | 120–300 ms | 15–20x faster |
| Cache Hit Ratio | 85–95% | N/A | Reduces origin load |
| TLS Handshake | 1–5 ms (nearby PoP) | 50–150 ms | 10–30x faster |
| Bandwidth Cost | $0.01–0.04/GB | $0.05–0.12/GB | 3–5x cheaper |
| Availability | 99.99% (multi-PoP) | 99.9% (single DC) | 10x fewer outages |
Stale serving decouples delivery from validation. The edge answers from cache instantly, then reconciles with the origin asynchronously. For a deeper look at how TTLs and validation interact, see our CDN fundamentals guide and architecture overview.
Both directives were standardized in RFC 5861 and are now supported natively by Cloudflare, Fastly, Akamai, CloudFront, and modern browsers. They are not a hack; they are part of Cache-Control and CDN-Cache-Control semantics.
stale-while-revalidate: Hide Revalidation Latency
stale-while-revalidate (SWR) tells a cache: after max-age expires, you may serve the stale object for N additional seconds while you revalidate in the background. The client gets an instant stale response; the next client gets the fresh one.
How the state machine works
Consider Cache-Control: public, max-age=60, stale-while-revalidate=300:
- 0-60s: Fresh. Edge serves HIT, no origin contact.
- 60-360s: Stale but revalidatable. Edge serves STALE (often with
Ageheader > 60) and fires an async conditional request (If-None-Match/If-Modified-Since) to origin. If origin returns 304, the object is refreshed. If 200, it is replaced. - >360s: Beyond SWR window. Edge must block and revalidate synchronously.
The latency win is immediate. In production traces across edge networks, SWR reduces p95 TTFB for HTML from 250-400ms to 25-50ms during the revalidation window because no client waits for origin.
Cache-Control: public, max-age=60, stale-while-revalidate=300
CDN-Cache-Control: public, max-age=300, stale-while-revalidate=86400
ETag: "a3f8-20260907"
Surrogate-Key: product-4821 price-feed
Note the split: browsers get a short max-age for freshness, while the CDN gets a longer one via CDN-Cache-Control (or Surrogate-Control on Fastly). This layered approach is critical for Core Web Vitals — you keep LCP fast at the edge without forcing browsers to hold stale HTML too long.
When to use SWR
SWR is ideal where slightly stale is acceptable and revalidation cost is high: SSR HTML, API responses for feeds and listings, product catalog pages, and image optimization manifests. It is less appropriate for personalized checkout, auth tokens, or inventory counts where staleness equals oversell.
Set the SWR window proportional to your origin update frequency and revalidation latency. A common pattern is max-age at 10-20% of SWR. For a news homepage updated every 60s, max-age=10, stale-while-revalidate=60 keeps 90% of requests instant while guaranteeing refresh within a minute.
stale-if-error: Survive Origin Failures
stale-if-error (SIE) is your circuit breaker. It says: if revalidation fails with an error (500, 502, 503, 504, or network failure), you may serve stale for N additional seconds rather than propagating the error.
Example: Cache-Control: public, max-age=60, stale-while-revalidate=300, stale-if-error=86400 means that even if the object is 12 hours stale, the edge will still serve it during an origin outage rather than returning a 500 to users. For many content sites, a 12-hour-old article is infinitely better than an error page.
SIE does not trigger on 4xx. A 404 or 403 is considered authoritative — the origin is saying the resource does not exist or is forbidden, so the cache should not mask it. Only 5xx and timeouts qualify. Some CDNs let you extend this to include 421, 429, or specific timeout thresholds; verify your provider's mapping.
if (origin_status in [500,502,503,504] or origin_timeout) {
if (age < max-age + stale-if-error) return STALE;
else return ERROR;
}
Operationally, SIE turns a hard dependency on origin availability into a soft one. Teams that set stale-if-error=86400 to 604800 (1-7 days) for static assets and SSR shells routinely ride through 10-30 minute origin deploys, database failovers, and regional brownouts with zero user impact. Pair it with origin shielding and health checks so stale is a last resort, not a crutch for an unhealthy origin.
| Directive | Trigger | Serves Stale? | Typical Window | Use Case |
|---|---|---|---|---|
| max-age | Age < max-age | No — fresh only | 10s – 5m (HTML), 1h – 1y (assets) | Baseline freshness |
| stale-while-revalidate | max-age expired, origin reachable | Yes + async refresh | 60s – 1 day | Hide revalidation latency |
| stale-if-error | Revalidation returns 5xx / timeout | Yes instead of error | 1h – 7 days | Resilience during outages |
| must-revalidate / no-cache | Expired + no SWR/SIE | No — must block | — | Strict freshness (checkout, auth) |
Implementation Across CDNs and Origins
Origin headers are the source of truth, but every CDN has an override layer. Configure both so behavior is predictable if headers are stripped.
1. Origin headers (recommended default)
res.setHeader('Cache-Control', 'public, max-age=60, stale-while-revalidate=300, stale-if-error=86400');
res.setHeader('CDN-Cache-Control', 'public, max-age=300, stale-while-revalidate=86400, stale-if-error=604800');
# Nginx
add_header Cache-Control "public, max-age=60, stale-while-revalidate=300, stale-if-error=86400" always;
add_header CDN-Cache-Control "public, max-age=300, stale-while-revalidate=86400, stale-if-error=604800" always;
# Varnish VCL
sub vcl_backend_response {
set beresp.ttl = 60s;
set beresp.grace = 24h; # maps to stale-if-error
set beresp.keep = 7d;
}
2. CDN overrides
- Cloudflare: Cache Rules → Eligible for cache → Edge TTL + Stale while revalidate / Stale if error. Honors origin
stale-while-revalidateby default when “Respect origin headers” is enabled. - Fastly:
beresp.grace(SIE) andberesp.stale_while_revalidate. Set via VCL or Compute.Surrogate-Control: max-age=300, stale-while-revalidate=86400is respected. - Akamai: Property Manager → Caching → “Serve Stale” options. Maps to
stale-while-revalidateandstale-if-errorbehaviors. - CloudFront: Cache policy + Origin request policy. Enable “StaleWhileRevalidate” and “StaleIfError” custom headers via Functions or origin headers; CloudFront now forwards and honors RFC 5861 directives when using Cache-Control origin.
Always emit ETag or Last-Modified so background revalidations can be conditional (304). Without validators, every SWR triggers a full 200 re-download, erasing bandwidth savings.
3. Browser vs edge separation
Use CDN-Cache-Control to give edges a longer SWR window than browsers. Browsers should not hold stale HTML for hours. Edges can. This avoids the classic mistake of setting stale-while-revalidate=86400 globally and then wondering why users see yesterday’s price after a hard refresh bypasses the CDN but not the browser cache.
Tuning, Risks, and Cache Poisoning Defense
Stale serving is powerful but amplifies mistakes. A poisoned object cached once can be served stale for days via SIE.
- Never apply SIE to unvalidated responses. Only cache 200/203/206/304 and explicitly validated 404s. Ensure your origin does not cache 500s. Set
Cache-Control: no-storeon 5xx at origin; let SIE handle stale at the edge instead. - Key correctly. Include
Vary: Accept-Encoding, Acceptand any personalization keys. SWR on a cache key that collapses authenticated and anonymous variants will leak stale private data. - Monitor stale ratios. Export
Cache-StatusorX-Cacheheaders (hit; stale; revalidated) to your observability stack. Alert if stale-if-error rate > 1% or if SWR revalidation error rate spikes. - Purge with care. A purge should invalidate both fresh and stale copies. Verify your CDN purges grace/SWR copies; some require
soft purgevshard purgedistinction. - Test failure mode. Simulate origin 503 with a chaos test and confirm edge serves stale with
Warning: 110 - Response is Stalerather than failing. Document expected staleness SLA for stakeholders.
When configured with layered TTLs, conditional revalidation, and proper cache keys, stale-while-revalidate and stale-if-error shift your CDN from a simple cache into a resilience tier — one that absorbs latency and outages so your origin does not have to be perfect to deliver a perfect user experience.
Frequently Asked Questions
What is the difference between stale-while-revalidate and stale-if-error?
stale-while-revalidate serves stale while the origin is healthy to hide revalidation latency — the edge refreshes in the background. stale-if-error serves stale only when revalidation fails with a 5xx or timeout, acting as a failover to keep the site online during outages. Most production configs use both together.
How long should stale-while-revalidate and stale-if-error be?
For HTML/SSR, start with max-age=60, stale-while-revalidate=300, stale-if-error=86400 and longer at the edge via CDN-Cache-Control: max-age=300, stale-while-revalidate=86400, stale-if-error=604800. For immutable assets (hashed JS/CSS/images), use max-age=31536000, immutable and skip stale directives. Adjust based on how stale your content can safely be.
Does stale-while-revalidate work in browsers?
Yes. Modern browsers honor stale-while-revalidate for HTTP cache, but support is best at CDNs and service workers. Use CDN-Cache-Control to give CDNs a longer SWR window than browsers, so users do not see overly stale HTML after a local refresh.
Can stale serving cause cache poisoning to persist longer?
Yes, if you cache an incorrect response and allow long SIE windows, the poisoned object will be served stale for the full window. Mitigate by never caching 5xx, requiring ETag/Last-Modified validators, scoping cache keys correctly with Vary, and ensuring purges clear both fresh and stale copies. Monitor Cache-Status headers for unexpected stale hits.