Stale-While-Revalidate Cache-Control: Hide Origin Latency Without Serving Bad Data
How to use the stale-while-revalidate Cache-Control directive to serve cached responses instantly while revalidating in the background. RFC 5861 recipes, CDN support in 2026, and footguns to avoid.
Stale-while-revalidate is a Cache-Control directive that lets a browser or CDN serve a cached response instantly while fetching a fresh copy in the background. You configure it by adding stale-while-revalidate=<seconds> alongside max-age, and the result is near-zero TTFB for cached routes plus automatic refresh without a thundering herd. In 2026, all major CDNs honor it, including Cloudflare, which finally made asynchronous revalidation generally available. This guide covers the spec, the header recipes I ship in production, and the CDN footguns that still bite.
stale-while-revalidate (RFC 5861) defines a grace window where caches serve stale content while revalidating asynchronously, eliminating the cliff at max-age expiry.
The directive is supported by Chrome 75+, Firefox 68+, and Safari 14+ in browsers; CDN support now covers Cloudflare, Fastly, Google Cloud CDN, AWS CloudFront, and Akamai.
Cloudflare's asynchronous revalidation is GA in 2026: the first request after expiry no longer pays the origin round-trip; it gets stale immediately with an UPDATING cache status.
The canonical recipe for HTML is Cache-Control: public, max-age=0, s-maxage=60, stale-while-revalidate=86400, fresh on the edge for 60s, stale-served for 24h while the origin refreshes.
Next.js prior to v15 wrote a non-compliant stale-while-revalidate with no duration; v15+ emits swrDelta (default 1 year) so the header works on any spec-compliant CDN, not just Vercel.
Pair it with stale-if-error to keep serving cached content during origin outages. Same syntax, different trigger.
What is stale-while-revalidate in HTTP?
stale-while-revalidate is a Cache-Control response directive standardized in RFC 5861 that grants a cache permission to serve a response after its max-age freshness window has expired, on the condition that the cache simultaneously revalidates the entry against the origin. The user never waits. The cache catches up in the background.
That sounds modest until you trace what happens without it. With plain max-age=60, the 61st second after an entry expires is a cliff: the next request blocks on the origin round-trip, the response is missed in the cache for any concurrent visitors, and if origin response time is 400ms, you've just spent 400ms of TTFB on a page that could have been served in 4ms. Multiply by traffic and you get the classic "cache stampede," a herd of users all paying origin latency simultaneously while the cache rebuilds.
The RFC was published in 2010, but adoption was slow because not every cache implementation handled the asynchronous half correctly. Some CDNs treated stale-while-revalidate as advisory and still blocked the first request. Others required vendor-specific headers. As of 2026, that's largely behind us. Every tier-1 CDN now does what the spec describes, and the directive has become the default tool for any cacheable response where origin latency is non-trivial.
How does stale-while-revalidate work?
A cache that supports the directive tracks every cached response in one of three states, derived from the entry's age relative to the max-age and stale-while-revalidate values:
Fresh: age < max-age. The cache serves the entry directly with no validation. This is the fastest path; TTFB is essentially network latency plus a cache lookup.
Stale-but-usable: max-age ≤ age < max-age + stale-while-revalidate. The cache serves the stale entry immediately and asynchronously kicks off a revalidation request to the origin. The user pays nothing for the refresh; the next user gets the updated copy.
Rotten: age ≥ max-age + stale-while-revalidate. The cache must validate before serving. The user pays the full origin round-trip. Same behavior as if stale-while-revalidate were never set.
The asynchronous step is where the design wins. A CDN that does this well will also coalesce concurrent revalidation requests, so that 10,000 simultaneous edge requests for an expired entry produce one origin fetch, not ten thousand. Per the Cloudflare cache revalidation docs, the first request after expiry receives stale with an UPDATING cache status header, every concurrent request gets the same stale response with the same status, and only after the origin replies do subsequent requests transition to HIT on the fresh entry. Zero requests block on origin.
max-age vs s-maxage vs stale-while-revalidate
Three directives collaborate to define cache behavior across browsers and CDNs, and confusing them is the most common reason a "perfectly tuned" cache header behaves nothing like the engineer expected.
Directive
Who reads it
What it controls
Typical value
max-age
Browser + shared caches (if no s-maxage)
Freshness duration in seconds
0 for HTML, 31536000 for hashed assets
s-maxage
Shared caches only (CDN, reverse proxy)
Freshness duration, overrides max-age at the edge
60 to 3600 for HTML
stale-while-revalidate
Browser + shared caches that support RFC 5861
How long to serve stale during async revalidation
5× to 100× the freshness value
stale-if-error
Browser + shared caches that support RFC 5861
How long to serve stale when origin returns 5xx or times out
Hours to days
public / private
All caches
Whether shared caches may store the response
public for shared content, private for per-user
The key insight: max-age governs the fresh window, and stale-while-revalidate governs the grace window after freshness expires. They add, they don't overlap. So max-age=60, stale-while-revalidate=3600 gives you 60 seconds where the response is served with no checks, plus 60 more minutes where it's served stale while the cache refreshes. Total time the entry can be served from cache: 3,660 seconds. Anything beyond that is a rotten entry and forces a synchronous revalidation.
Header recipes I ship in production
The boilerplate I reach for first depends on what's being cached. These four cover roughly 90% of routes I configure.
Browser never caches HTML (so users see the latest after a navigation), CDN treats it as fresh for 60 seconds, and the CDN keeps serving stale for 24 hours while it revalidates in the background. Origin gets hit once per minute per edge POP regardless of traffic. TTFB on a cache hit is around 10ms; on a stale revalidation it's also around 10ms because the user doesn't wait for the origin.
Lets the browser hold for 30 seconds (avoids re-fetching on rapid back-navigation), CDN holds fresh for 2 minutes, stale-served for 10 more, and crucially stale-if-error keeps the API "up" for a day if origin starts returning 5xx. Honestly, this is the recipe that turns a 10-minute origin outage into a non-event for cached endpoints.
No stale-while-revalidate needed. The URL changes when the content changes, so the cached entry never goes stale. immutable tells browsers not to even bother sending conditional revalidations on reload.
private keeps it out of shared caches entirely, but the browser still gets the SWR benefit on the next navigation. Useful for personalized dashboards where the prior render is "good enough" to paint immediately while the fresh one loads.
CDN support in 2026: Cloudflare, Fastly, CloudFront
Every major CDN now implements stale-while-revalidate per spec, but the rollout history matters because outdated blog posts still claim otherwise. So here's the 2026 state of the world.
Cloudflare
This is the big change. Asynchronous stale-while-revalidate went generally available across Free, Pro, and Business zones in 2025, with Enterprise on the same path. Cloudflare honors the directive as long as your origin sends it; there is no dashboard toggle for it on default cache rules. The response includes cf-cache-status: UPDATING while revalidation is in flight, then transitions to HIT on subsequent requests. If you were avoiding Cloudflare for SWR-dependent workloads, that calculus has changed.
Fastly
Fastly has supported SWR since 2018 via standard Cache-Control, and via Surrogate-Control if you want stale behavior only at the edge without exposing it to browsers. Fastly's implementation also coalesces concurrent revalidation through request collapsing, which is on by default.
AWS CloudFront
CloudFront added SWR support in 2023 and it works exactly as the spec describes. The only caveat is that CloudFront's behavior interacts with the Origin Shield layer: if Origin Shield is enabled, revalidation requests funnel through one POP, which is usually what you want for cache-fill efficiency.
Google Cloud CDN
Honors stale-while-revalidate on responses from cacheable origins. The one exception, per the Cloud CDN docs, is FORCE_CACHE_ALL mode, which ignores origin Cache-Control entirely and therefore ignores SWR. If you've set that mode, you're managing TTLs yourself.
Akamai
Akamai exposes equivalent behavior under "stale-while-revalidate" and "stale-if-error" properties in Property Manager, and respects the RFC 5861 directives when the cache rule allows it. The naming matches the standard.
stale-while-revalidate vs Next.js ISR
Next.js's Incremental Static Regeneration (ISR) is essentially SWR with a different storage layer. Pre-rendered pages live on disk (or in a KV store on Vercel), and after their revalidate window expires, the next request triggers a background regeneration while the current visitor sees the stale build. The semantics overlap so heavily that the two are sometimes used interchangeably, which causes problems when self-hosting.
The pitfall: before Next.js 15, the framework emitted a non-compliant Cache-Control: s-maxage=60, stale-while-revalidate header with no value on the SWR directive. RFC 5861 says the directive without a delta-seconds value is invalid, so spec-compliant CDNs like Cloudflare and CloudFront treated the response as if SWR weren't set at all. Only Vercel's edge, which had its own non-standard parsing, honored it. The fix was experimental.swrDelta in next.config.js, which sets the missing value.
From Next.js 15 onward, the framework emits a fully-formed header by default, typically stale-while-revalidate=31536000 (one year) for ISR pages. That means self-hosted Next.js on Cloudflare, Fastly, or CloudFront now gets the same edge-cache behavior as on Vercel, without configuration. If you're on an older Next.js version and wondering why ISR "doesn't work" outside Vercel, this is almost certainly why. I covered the broader edge-caching picture in my Early Hints TTFB guide; SWR and Early Hints are complementary, not redundant.
Pairing with stale-if-error
The under-used sibling of stale-while-revalidate is stale-if-error, defined in the same RFC. It lets the cache serve a stale response when revalidation fails (origin times out, returns 5xx, or the connection drops) instead of bubbling the error to the user.
That last value says: for up to 24 hours after the entry rots, if the origin is unreachable or erroring, keep serving the stale copy. Combined with SWR, it's the difference between "100% downtime for 10 minutes" and "logged-in users see slightly outdated data for 10 minutes." For content sites and cacheable APIs, the user-facing impact of a partial origin outage drops to near zero.
Browser support for stale-if-error is narrower than for SWR. Chrome and Firefox respect it on the network cache, Safari is still inconsistent, but CDN support is essentially universal across the same vendors listed above. Since most outages affect the origin and CDNs sit in front, CDN-side support is what matters.
Footguns to avoid
Caching personalized content publicly
If your response contains anything user-specific (session-aware HTML, personalized prices, A/B test variants) and you ship public, max-age=<anything>, stale-while-revalidate=<anything>, the CDN will serve user A's response to user B. Either use private or properly key the cache with Vary. I once watched a deploy ship Cache-Control: public on a logged-in dashboard route; within four minutes, every user saw someone else's name. SWR makes this worse because the wrong response sticks around for the full SWR window.
Setting the SWR window too short
If stale-while-revalidate is shorter than your average traffic gap between requests, you're back to synchronous origin fetches. For a low-traffic route hit once an hour, stale-while-revalidate=60 does nothing: by the time the next user arrives, the entry is rotten, and they pay full origin latency. Default to "longer than you think" and only tighten if you observe a freshness problem.
Forgetting that revalidation still hits the origin
SWR doesn't make origin requests disappear; it decouples them from user requests. If your origin can't handle the request rate generated by background revalidations (one per edge POP per s-maxage interval), you'll see origin load go up after enabling SWR, not down. The fix is usually request collapsing at the CDN layer, which most vendors enable by default. Pair this with the Service Worker Static Routing API to further reduce origin traffic for repeat visitors.
Mistaking the SWR npm library for the HTTP directive
The popular React data-fetching library SWR takes its name and inspiration from this Cache-Control directive but is unrelated at the protocol level. It implements similar semantics in the browser tab's memory. If you're debugging cache behavior and someone says "we're using SWR," ask which one. They solve different layers of the same problem and stack just fine.
Cache-busting on every deploy
If your deploy pipeline issues a CDN-wide purge on every release, SWR can't help you. The cache is empty after every deploy and the next request pays origin latency. Prefer surgical purge (by URL or tag) over full purge, or rely on long max-age with content-hashed URLs so old assets remain cacheable. The same logic that protects the LCP sub-part timings on hot paths applies here: every cache miss is a hidden cost.
Frequently Asked Questions
Does Cloudflare support stale-while-revalidate in 2026?
Yes. Cloudflare's asynchronous SWR went generally available across Free, Pro, and Business tiers in 2025 and continues to roll out on Enterprise. As long as your origin response includes Cache-Control: stale-while-revalidate=<seconds> alongside max-age or s-maxage, Cloudflare will serve stale content with a cf-cache-status: UPDATING header while revalidating in the background.
What is the difference between max-age and stale-while-revalidate?
max-age defines the freshness window during which a cached response is served with no validation. stale-while-revalidate defines an additional grace window after freshness expires during which the cache may serve the stale response while asynchronously fetching a fresh copy. The two values add: max-age=60, stale-while-revalidate=3600 means the entry can be served from cache for up to 3,660 seconds total.
Can stale-while-revalidate be used with personalized content?
Yes, but use private instead of public so the response stays out of shared CDN caches and only the user's own browser benefits. Without private, a CDN may serve one user's personalized response to another. If you need to cache personalized variants at the edge, use a properly-tuned Vary header or signed cache keys.
What is stale-if-error?
stale-if-error is the companion directive from RFC 5861 that lets a cache serve a stale response when revalidation fails (origin returns 5xx, times out, or the network drops). Syntax matches SWR: stale-if-error=86400 serves stale for 24 hours during an outage. Pair it with stale-while-revalidate to make cached endpoints resilient to brief origin failures.
How do I verify stale-while-revalidate is working?
Request the URL twice past the max-age window and inspect the response headers. The first stale request should return an Age value greater than max-age and a cache-status header indicating revalidation in progress (cf-cache-status: UPDATING on Cloudflare, x-cache: REVALIDATING on CloudFront, similar on others). A subsequent request after a few seconds should show a fresh Age close to zero. If the Age never crosses max-age, the cache isn't keeping the entry around long enough for SWR to apply.
Client Hints (Sec-CH-DPR, Sec-CH-Width, Critical-CH) let your CDN pick byte-perfect image sizes in 2026. Practical setup, worker code, and real LCP wins from a Chrome-heavy audit.
Learn how RFC 9211's Cache-Status header exposes hits, misses, forwards, and coalesced requests across your CDN chain. Includes vendor mapping and TTFB attribution patterns.
Tag any image, price, or button with elementtiming and read its real paint timestamp from PerformanceObserver. Practical RUM setup, cross-origin gotchas, and the new Container Timing trial.