Skip to main content
Edge Caching with Stale-While-Revalidate: Fast and Fresh at the CDN

Edge Caching with Stale-While-Revalidate: Fast and Fresh at the CDN

KU
· Founder & Chief Editor
• 8 min read•Updated •36 views

The cache-control header most teams under-use. How stale-while-revalidate and stale-if-error turn a CDN from a freshness liability into a latency and resilience win, with the gotchas.

Key takeaways

  • Send Cache-Control public, max-age=60, stale-while-revalidate=600, stale-if-error=86400 for shared content.
  • After 60 seconds the CDN serves the cached copy instantly and refetches it in the background, and during origin 5xx errors it keeps serving the old copy for a day.
  • Pair long stale windows with event-driven purge for urgent changes.

stale-while-revalidate lets a CDN serve an expired copy instantly while it refetches from origin in the background. Send Cache-Control: public, max-age=60, stale-while-revalidate=600, stale-if-error=86400 and no visitor waits on origin at the TTL boundary, and a 5xx from origin keeps serving the cached page for a day.

Verified against

RFC 5861 and RFC 9111, Cloudflare Cache-Control docs, Amazon CloudFront expiration docs, against the rfc-editor.org texts and vendor developer docs. Checked October 2026.

Most teams pick one extreme: no-cache everything (slow, origin-bound) or cache aggressively and fight stale-content bugs. These two directives, defined in RFC 5861, let you stop choosing. It is the highest-value header change we know of for a content site.

The problem with binary caching

A plain max-age=60 works like this: for 60 seconds the edge serves the cached copy. At second 61, the next visitor waits for a full origin round-trip while the cache refills. That one visitor pays for everyone else's freshness, and under bursty traffic it shows up as a latency spike at every TTL boundary, multiplied by every edge location that expires independently.

stale-while-revalidate: serve stale, refresh in the background

code
Cache-Control: public, max-age=60, stale-while-revalidate=600

Fresh for 60 seconds. For up to 600 seconds after that, the cache may serve the stale copy immediately and revalidate asynchronously. RFC 5861 describes exactly this: the response is served stale "while an asynchronous validation" runs, hiding the latency from the user. The visitor who hits the expired object gets the old content at edge speed; the next visitor gets the refreshed copy.

Two properties are easy to miss. Revalidation only happens when a request arrives, so a page nobody visits for 11 minutes simply expires and the next hit goes to origin. And the stale window is per edge location: a low-traffic region can still pay the miss.

stale-if-error: resilience for one directive

code
Cache-Control: public, max-age=60, stale-while-revalidate=600, stale-if-error=86400

stale-if-error=86400 says: if revalidation fails, keep serving the stale copy for up to a day instead of propagating the error. CloudFront's docs apply it when the origin is unreachable or returns a status between 500 and 600. Cloudflare's docs trigger it on origin 5xx only and say a 404 does not count. For marketing pages and docs, that turns a short origin outage into something visitors never see.

How the big CDNs actually honor it

The RFC says "may". Each CDN adds its own rules, and a few of them silently disable the behavior:

CDNHonors stale-while-revalidateHonors stale-if-errorGotcha that turns it off
CloudflareYes, revalidates in background; requests during refresh show UPDATINGYes, origin 5xx onlyAdding s-maxage disables SWR; Always Online ignores both; Workers Cache API unsupported
Amazon CloudFrontYes, asynchronous fetchYes, origin unreachable or 5xxCapped by the cache behavior's Maximum TTL
Browser cacheVaries by browser (see MDN compatibility table)Varies; MDN treats 500, 502, 503, 504 as errorsApplies to the private cache, separate from the CDN

The Cloudflare s-maxage rule is the one that bites. Its docs say that with s-maxage present a shared cache must revalidate before serving stale, so public, s-maxage=60, stale-while-revalidate=600 gives you no background refresh there. If you need separate browser and edge TTLs on Cloudflare, set the edge TTL with a Cache Rule instead. Values must be integers; Cloudflare says a fractional stale-while-revalidate is ignored and may bypass the cache.

Verify it from the command line

Check the header and the cache status yourself rather than trusting the config:

Terminal
curl -sI https://www.example.com/docs/ | grep -iE 'cache-control|cf-cache-status|age|x-cache'
cache-control: public, max-age=60, stale-while-revalidate=600, stale-if-error=86400
cf-cache-status: HIT
age: 41

Request again after age passes 60. On Cloudflare, expect cf-cache-status: UPDATING or STALE while the refresh runs, then HIT with a small age. On CloudFront, look at x-cache: Hit from cloudfront and the age header. If you only ever see MISS or DYNAMIC, the response is not cacheable at all, usually because of Set-Cookie, private, or a missing cache rule.

The cache key determines correctness

None of this is safe if the cache key is wrong. The key must include every input that changes the response, and nothing else:

  • Vary on what matters. If you serve different content by Accept-Language or auth state, that must be in the key, or one visitor gets another's page.
  • Strip what doesn't. Marketing parameters such as utm_* and fbclid fragment the cache into near-zero hit rate. Exclude them from the key.
  • Never cache authenticated responses on a shared key. Cookie-bearing responses need private or a per-user key. The classic CDN incident is caching a logged-in page and serving it to the world.

Purge is the freshness escape hatch

A long stale window widens the time stale content can be served, so for content that must change now (a price, a corrected article, a legal takedown) TTL is not enough. Pair long stale windows with event-driven purge from your publish hook. On Cloudflare:

Terminal
curl https://api.cloudflare.com/client/v4/zones/$ZONE_ID/purge_cache \
    -H 'Content-Type: application/json' \
    -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
    -d '{"files": ["https://www.example.com/article/123"]}'

Cloudflare notes that a 200 with success: true means the purge was accepted, not completed; confirm with a request that shows cf-cache-status: MISS. If your cache key includes headers, the purge request must send the same header values. TTL handles routine freshness; purge handles urgent freshness. For the wider tradeoffs, see CDN cache invalidation strategies.

A config to start from

code
# Static-ish HTML: docs, marketing, blog
Cache-Control: public, max-age=60, stale-while-revalidate=600, stale-if-error=86400

# Fingerprinted assets (hashed filenames)
Cache-Control: public, max-age=31536000, immutable

# Per-user responses
Cache-Control: private, no-cache

Good fits: marketing pages, docs, blog content, product listings that tolerate seconds of staleness, API responses for slow-changing reference data.

Bad fits: per-user dashboards, anything where stale data is a correctness or compliance problem (account balances, inventory at checkout), and responses that set cookies.

Common failure modes

  • Header present, no stale serving on Cloudflare. You also sent s-maxage, or Always Online is on. Remove one.
  • stale-if-error never kicks in. The origin returned a 404 or a 200 error page, not a 5xx, or you also sent no-cache or must-revalidate, which Cloudflare says override it.
  • Stale content outlives the window on CloudFront. It doesn't: CloudFront serves stale for the lesser of the directive and the behavior's Maximum TTL, so a short Maximum TTL silently caps you.
  • A user sees someone else's account page. The cache key ignored a cookie or auth header. Mark it private and purge.

Where this breaks

Stale-while-revalidate assumes serving slightly old content is harmless. For prices at checkout, stock levels, auth state, or anything regulated, it isn't, and you should not cache those on a shared key at all. It also does little for long-tail pages that get one hit an hour, because revalidation needs traffic to trigger. And it does not replace application caching: the cache-aside, read-through and write-through patterns still matter for what the origin does on a miss.

Questions people ask

What does stale-while-revalidate do?

It lets a cache keep serving a response for a set number of seconds after max-age expires, while it fetches a fresh copy in the background. With max-age=60, stale-while-revalidate=600, requests between 60 and 660 seconds get the cached copy instantly and trigger an asynchronous refresh. The directive comes from RFC 5861, and both Cloudflare and CloudFront honor it.

Does s-maxage work with stale-while-revalidate?

On Cloudflare, no. Its docs state that s-maxage disables stale-while-revalidate, because RFC 9111 says a shared cache must not serve stale content without revalidating when s-maxage is present. Use max-age with the stale directives, and set a separate edge TTL with a Cache Rule if you need it. CloudFront uses s-maxage for its TTL and documents SWR independently.

What is the difference between stale-while-revalidate and stale-if-error?

stale-while-revalidate covers the normal case: the copy expired, so serve it now and refresh in the background. stale-if-error covers failure: revalidation hit an error, so keep serving the old copy for up to the given seconds. CloudFront counts an unreachable origin or a 5xx as an error; Cloudflare counts 5xx only, not 404.

The call we'd make

Default every shareable HTML route to public, max-age=60, stale-while-revalidate=600, stale-if-error=86400, fingerprint and immutable your assets, and mark anything per-user private. Get the cache key right first, wire purge into your publish path, and check with curl -sI that your CDN is actually serving stale. If you are on Cloudflare, leave s-maxage out of these headers.

Sources

Explore topics:Cloud
React
Share this post
Cite this article
Plain text

Kiril Urbonas (2026). Edge Caching with Stale-While-Revalidate: Fast and Fresh at the CDN. DevOpsNess. Updated 2026-10-31. https://www.devopsness.com/blog/edge-caching-stale-while-revalidate

Markdown

[Edge Caching with Stale-While-Revalidate: Fast and Fresh at the CDN](https://www.devopsness.com/blog/edge-caching-stale-while-revalidate) (Kiril Urbonas, DevOpsNess, updated 2026-10-31)

KU

Kiril Urbonas

Founder & Chief Editor

590 articles
View all articles by Kiril Urbonas

You might have missed

Evergreen posts worth revisiting.