Loading learning content...
In traditional caching, there's an awkward moment when cached content expires. The user's request arrives, the cache realizes its content is stale, and the user must wait while the cache fetches fresh content from the origin. For that unlucky user, the cache provides zero benefit—they experience the full origin latency as if the cache didn't exist.
The stale-while-revalidate (SWR) pattern elegantly solves this problem. Instead of making the user wait, the cache immediately serves the stale content while simultaneously refreshing in the background. The current user gets an instant response (with slightly dated content), and subsequent users get fresh content.
This seemingly simple shift in strategy has profound implications. SWR effectively eliminates latency spikes at cache boundaries, provides natural protection against origin failures, and enables aggressive caching without sacrificing content freshness. It's one of the most impactful optimizations in modern CDN architecture.
By the end of this page, you will understand the exact mechanics of stale-while-revalidate, how to configure it across different CDN providers, the subtle edge cases that can cause problems, and how to combine SWR with other caching strategies for optimal performance.
To appreciate stale-while-revalidate, we must first understand the fundamental problem it addresses: cache expiration latency spikes.
The Traditional Cache Expiration Problem:
Consider a product page cached for 5 minutes. During those 5 minutes, every user gets instant sub-50ms responses. But the moment the cache expires:
Result: That user waited 800ms instead of 50ms. And if this is a popular page, potentially hundreds of users could be blocked simultaneously during this refresh window.
1234567891011121314151617181920212223
LATENCY OVER TIME (Traditional Caching with max-age=300): Latency (ms)│800 │ × × │ /| /|600 │ / | / | │ / | / |400 │ / | / | │ / | / |200 │ / | / | │ / | / | 50 │────────────· ·────────────· ·──── │ └───────────────────────────────────────────────→ Time 0 5min 5:01min 10min 10:01min PATTERN:- Perfect latency (50ms) during cache-fresh period- Latency spike (800ms) exactly at cache expiry- First request after expiry pays the full origin cost- Subsequent requests are fast again- Cycle repeats every 5 minutesThe Thundering Herd Problem:
The situation worsens under high traffic. When cache expires:
User Experience Impact:
Latency spikes are particularly harmful because:
Without SWR, caches create a 'cliff' at expiration where performance falls off dramatically. For content with regular update patterns (e.g., blog posts updated at 9 AM), this cliff can affect many users simultaneously. SWR smooths out these cliffs into gentle slopes.
Stale-while-revalidate introduces a grace period after the cache's TTL expires during which stale content can still be served while a background refresh occurs.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
CACHE-CONTROL: max-age=300, stale-while-revalidate=3600 TIMELINE:═══════════════════════════════════════════════════════════════════════ T = 0 seconds├─ Response cached├─ "Fresh" period begins└─ Any request → Instant cache hit (fresh content) ───────────────────────────────────────────────────────────────────── T = 0 to T = 300 seconds (FRESH PERIOD)├─ All requests served instantly from cache├─ No origin requests needed└─ No revalidation ───────────────────────────────────────────────────────────────────── T = 300 seconds (TTL EXPIRES)├─ Content now "stale"├─ SWR window begins└─ Content can still be served, but with background refresh ───────────────────────────────────────────────────────────────────── T = 300 to T = 3900 seconds (STALE-WHILE-REVALIDATE WINDOW)│├─ Request arrives at T = 350s:│ ├─ Cache immediately returns stale content (instant response!)│ ├─ Cache triggers background request to origin│ ├─ Origin returns fresh content│ └─ Cache updates stored content for future requests│├─ Request arrives at T = 355s:│ └─ Cache returns fresh content (updated by previous background refresh)│└─ If no requests during this window, content stays stale ───────────────────────────────────────────────────────────────────── T = 3900 seconds (SWR WINDOW EXPIRES: 300 + 3600)├─ Content too stale to serve├─ Next request MUST wait for fresh content└─ Synchronous revalidation required ═══════════════════════════════════════════════════════════════════════Key Mechanics of SWR:
Immediate Response: When a request hits stale content within the SWR window, the cache responds immediately with the stale content. No waiting.
Background Refresh: Simultaneously (or immediately after responding), the cache initiates a fresh request to the origin. This happens asynchronously.
Cache Update: When the origin responds, the cache updates its stored content. Subsequent requests receive this fresh content.
Window Expiration: The SWR window is finite. After max-age + stale-while-revalidate seconds, the cache must perform synchronous revalidation.
Single Flight: Smart implementations ensure only one background revalidation per stale object, even if multiple requests arrive during the stale period.
SWR has a fundamental trade-off: the user who triggers the background refresh receives stale content. They're 'one request behind' the fresh content. For most applications, this trade-off is excellent—near-instant latency for a small freshness delay. But for content where being even slightly behind is unacceptable, SWR may not be appropriate.
Stale-while-revalidate is configured via the Cache-Control header. The directive takes a single value: the number of seconds after TTL expiration during which stale content may be served.
123456789101112131415161718192021222324252627282930
# BASIC SWR CONFIGURATIONHTTP/1.1 200 OKCache-Control: max-age=60, stale-while-revalidate=300# Fresh for 1 minute, then stale+refresh for 5 minutes # AGGRESSIVE SWR (Availability-focused)HTTP/1.1 200 OKCache-Control: max-age=300, stale-while-revalidate=86400# Fresh for 5 minutes, then stale+refresh for 24 hours# Almost never blocks on origin # CONSERVATIVE SWR (Freshness-focused)HTTP/1.1 200 OKCache-Control: max-age=3600, stale-while-revalidate=60# Fresh for 1 hour, only 1 minute of SWR grace period# Quickly forces synchronous refresh if content is old # COMBINED WITH OTHER DIRECTIVESHTTP/1.1 200 OKCache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=3600, stale-if-error=86400# Browser: fresh 1 min (no SWR in most browsers)# CDN: fresh 5 min, SWR for 1 hour, error fallback for 24 hours # SPLIT BROWSER/CDN WITH SWR# Some CDNs support CDN-specific SWR while browsers don'tHTTP/1.1 200 OKCache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=3600 # Browser: Uses max-age=60, may or may not support SWR# CDN: Uses s-maxage=300, typically supports SWR| Platform | SWR Support | Notes |
|---|---|---|
| Cloudflare | ✓ Full support | Native SWR, configurable in Cache Rules |
| AWS CloudFront | ✓ Full support | Enabled via origin response headers |
| Fastly | ✓ Full support | Configurable in VCL |
| Akamai | ✓ Full support | Property Manager configuration |
| Chrome/Edge | ✓ Supported | Service Worker and HTTP cache |
| Firefox | ✓ Supported | Since Firefox 68 |
| Safari | ⚠ Partial | Limited support, check version |
| nginx | ✓ Configurable | Via proxy_cache_use_stale |
| Varnish | ✓ Configurable | Via VCL beresp.grace |
If you're new to SWR, start with a short SWR window (e.g., stale-while-revalidate=60) and monitor behavior. Once you're confident in your cache invalidation strategy, gradually increase the window. Very long SWR windows require robust purge mechanisms to update critical content.
Stale-while-revalidate has a closely related companion: stale-if-error. While SWR handles normal expiration, stale-if-error handles origin failures. Understanding both—and how they work together—is essential for a robust caching strategy.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
CONFIGURATION: max-age=300, stale-while-revalidate=3600, stale-if-error=86400 ═══════════════════════════════════════════════════════════════════════SCENARIO 1: Normal operation (origin healthy)═══════════════════════════════════════════════════════════════════════ T=0: Content cachedT=300: TTL expires, content staleT=350: Request arrives ├─ SWR applies: Serve stale instantly ├─ Background refresh succeeds └─ Content updatedT=355: Next request gets fresh content ✓ ═══════════════════════════════════════════════════════════════════════SCENARIO 2: Origin failure during SWR window═══════════════════════════════════════════════════════════════════════ T=0: Content cachedT=300: TTL expires, content staleT=350: Request arrives, origin is DOWN ├─ SWR applies: Serve stale instantly ├─ Background refresh fails (origin 503) ├─ stale-if-error applies: Keep serving stale └─ Retry origin on next requestT=500: Origin recovers └─ Next request refreshes successfully ✓ ═══════════════════════════════════════════════════════════════════════SCENARIO 3: Origin failure AFTER SWR window expires═══════════════════════════════════════════════════════════════════════ T=0: Content cachedT=300: TTL expiresT=3900: SWR window expires (300 + 3600) Content is now "too stale for SWR"T=4000: Request arrives, origin is DOWN ├─ SWR does NOT apply (window expired) ├─ Would need synchronous refresh, but origin down ├─ stale-if-error DOES apply (86400 seconds) └─ Serve stale content ✓ T=90000: stale-if-error window expires (300 + 86400) Origin still down └─ Must return 504 Gateway Timeout ✗ ═══════════════════════════════════════════════════════════════════════For production systems, always configure both stale-while-revalidate and stale-if-error. SWR handles performance; stale-if-error handles availability. Together, they provide a robust safety net that keeps your site responsive even during origin issues.
While stale-while-revalidate is powerful, several edge cases and implementation details can cause unexpected behavior. Understanding these prevents production surprises.
max-age=300 but is stored in cache for 200 seconds only has 100 seconds of freshness remaining. SWR window starts from that reduced freshness.123456789101112131415161718192021222324252627282930
SCENARIO: Multi-tier CDN with Age accumulation Origin Response: Cache-Control: max-age=300, stale-while-revalidate=3600 Origin → Shield (cached for 100s): Age: 100 Effective freshness remaining: 200s Shield → Edge (cached for 50s): Age: 150 Effective freshness remaining: 150s Edge → Browser (cached for 30s): Age: 180 Effective freshness remaining: 120s RESULT:User's browser sees content that's 180 seconds old with only 120 secondsof freshness remaining. SWR window starts after those 120 seconds. EFFECTIVE TIMELINE (from user's perspective):T=0: User receives response with Age: 180T=120: Content becomes stale (not T=300!)T=120 to T=3720: SWR window (stale-while-revalidate applies) MITIGATION:- Account for Age accumulation when setting TTLs- Consider longer max-age to ensure meaningful freshness at edges- Use short-TTL content in origin shield to reduce Age at edgesproxy_cache_use_stale updating for similar behavior. Syntax differs from HTTP standard.beresp.grace for stale serving. Conceptually similar but differently configured.CDN SWR implementations vary. Before relying on SWR in production, test your specific CDN's behavior: Does it actually serve stale and refresh in the background? Does it deduplicate background requests? What happens if the origin is slow vs. down? Document the specific behavior you observe.
Applying stale-while-revalidate effectively requires understanding both the technical mechanics and the operational implications.
stale-while-revalidate=3600. If 10 minutes is the maximum, use stale-while-revalidate=600.max-age=60, stale-while-revalidate=3600 refreshes every minute during traffic, but allows stale for quiet periods.stale-if-error should be longer.1234567891011121314151617181920212223242526272829303132333435363738394041
// Content update workflow with SWR // Step 1: Configure aggressive SWR for performance// In origin response:// Cache-Control: public, max-age=60, s-maxage=300, // stale-while-revalidate=86400, stale-if-error=86400 // Step 2: When content changes, explicitly purgeasync function updateProduct(productId, data) { // Update in database await database.products.update(productId, data); // Purge CDN cache for this product await cdnClient.purge([ `/products/${productId}`, `/products/${productId}.json`, `/api/products/${productId}`, ]); // With SWR + purge strategy: // - Normal traffic: Users get instant responses (stale during refresh) // - After update: Purge immediately clears stale content // - First request after purge: Cache miss, fetches fresh from origin // - Subsequent requests: Cache hit with fresh content return { success: true };} // Step 3: For critical updates, warm the cacheasync function updateAndWarmProduct(productId, data) { await database.products.update(productId, data); await cdnClient.purge([`/products/${productId}`]); // Immediately request the page to warm the cache // This ensures no user experiences origin latency await fetch(`https://cdn.example.com/products/${productId}`, { headers: { 'X-Cache-Warm': 'true' } }); return { success: true };}With proper SWR configuration, you can safely use very long CDN TTLs (hours or days) without worrying about serving stale content. The SWR pattern ensures refreshes happen during normal traffic, and purge handles explicit updates. This dramatically reduces origin load while maintaining freshness.
Implementing SWR is only valuable if you can measure its impact. Proper observability ensures SWR is working as expected and allows you to tune configurations.
| Metric | What It Measures | Target | Alert Threshold |
|---|---|---|---|
| SWR Hit Rate | % of responses served from stale cache during revalidation | < 10% of total cache hits | If > 30%, max-age may be too short |
| Background Refresh Time | Time to complete background origin fetch | < 1 second | If > 3s, origin performance issue |
| Stale Serving Duration | How old the content was when served stale | < 1.5x max-age | If > 2x, investigate refresh failures |
| Origin Error Rate | % of background refreshes that fail | < 0.1% | If > 1%, origin stability issue |
| Cache Hit Ratio | Overall % of requests served from cache | 95% for static | If < 90%, check TTL/SWR configuration |
12345678910111213141516171819202122232425262728293031323334
# Response served from fresh cacheHTTP/1.1 200 OKAge: 45X-Cache: HITX-Cache-Status: FRESHCF-Cache-Status: HIT # Cloudflare # Response served from stale cache (SWR in progress)HTTP/1.1 200 OKAge: 350 # Exceeds max-age of 300X-Cache: HITX-Cache-Status: STALEX-SWR: REVALIDATING # Custom header from edge logicWarning: 110 Response is stale # Response after background refresh completedHTTP/1.1 200 OKAge: 2 # Freshly refreshedX-Cache: HITX-Cache-Status: FRESHX-SWR: REFRESHED # Custom header indicating refresh happened # CLOUDFLARE-SPECIFIC CACHE STATUS VALUES:# HIT - Served from cache (fresh)# STALE - Served from cache (stale, with or without revalidation)# MISS - Not in cache, fetched from origin# EXPIRED - TTL exceeded, synchronous refresh required# REVALIDATED - Conditional refresh returned 304 # CUSTOM HEADERS FOR DEBUGGING (add via edge logic):X-Cache-Age: 350 # How old the cached content isX-Max-Age: 300 # Configured max-ageX-SWR-Window: 3600 # Configured stale-while-revalidateX-SWR-Time-Remaining: 3250 # Seconds left in SWR windowThe Age header reveals how old cached content is. If you see Age values consistently exceeding max-age, SWR is working—stale content is being served. Monitor these values to understand how often you're in SWR mode and whether your TTLs are appropriate for your traffic patterns.
Stale-while-revalidate is one of the most powerful patterns in CDN caching. It eliminates latency spikes, improves availability, and enables aggressive caching strategies.
What's Next:
We've mastered the mechanics of getting content into cache and keeping it fresh. But what about measuring and improving overall cache performance? The final page of this module covers Cache Hit Ratio Optimization—the strategies and techniques for maximizing the percentage of requests served from cache.
You now deeply understand stale-while-revalidate—how it works, when to use it, how to configure it, and how to monitor its effectiveness. This pattern should be part of virtually every CDN caching strategy where sub-second latency matters.