← Daily

Concept · No. 015 · October 30, 2026

Stale-while-revalidate moves cache misses off the request path

Plain max-age makes some unlucky user pay origin latency every time a cached response expires. stale-while-revalidate hands that cost to a background fetch, but it also quietly changes how stale your content can get.

The idea

With a plain Cache-Control: max-age, a cached response is either fresh or expired. Once it expires, the next request has to wait while the cache revalidates with the origin or fetches the response again. That visitor pays the full origin round trip. On a popular resource, the expiry hits every edge at roughly the same moment, and a burst of requests reaches the origin together.

stale-while-revalidate, defined in RFC 5861, adds a second window. For a set number of seconds after freshness ends, a cache may serve the stale copy immediately and refresh it in the background. The revalidation latency stops landing on any user.

The result is a freshness contract with three phases: fresh, stale but servable, and expired, where the request must wait.

An example

A product listing page is served through a CDN with Cache-Control: public, max-age=60, stale-while-revalidate=600.

  • At time zero the origin renders the page and the edge stores it.
  • For the next 60 seconds every request gets the fresh copy from the edge.
  • At 90 seconds a request arrives. The edge returns the stale copy instantly and starts one background fetch to the origin.
  • When that fetch completes, the edge replaces its copy and the freshness clock restarts.
  • If no request arrives for eleven minutes, the copy is past both windows, and the next visitor waits on the origin.

Under steady traffic, the origin sees about one request per minute per edge no matter how much load the page gets, and users almost never wait on a render.

Where it breaks

The background refresh is triggered by a request, not a timer. On a low traffic page, the first visitor after a long gap gets content as old as max-age plus the stale window. The real staleness bound is the sum, not max-age.

Support is uneven. Browsers honour it in their HTTP cache, while CDNs differ: some ignore it, and some do not collapse concurrent background fetches, so the stampede you wanted to avoid can return.

It is the wrong tool for personalised or authorised responses, and for data where staleness is a correctness bug: checkout prices, stock levels, revoked permissions. During the stale window, a change you already made is still being served.

It does nothing for a cold cache after a deploy or purge, and a failed background revalidation is a separate concern handled by stale-if-error.

In practice

I would start from the product question, not the origin's capacity: how old can this response be before someone is harmed or confused? Set max-age plus stale-while-revalidate to fit inside that answer.

  • Give the edge longer windows than the browser, using CDN-Cache-Control or your CDN's surrogate header. You can purge an edge, but you cannot purge a browser.
  • Pair long stale windows with purge on write for content that changes on known events.
  • Add stale-if-error so an origin outage degrades to old content instead of errors.
  • Return ETag or Last-Modified so background revalidations can end in a cheap 304.
  • Skip it for fingerprinted static assets. Those want a long max-age with immutable.
  • Log the Age header and cache status at the edge, and alert when Age exceeds the staleness budget you chose.
Treat max-age plus stale-while-revalidate as the real staleness bound, and choose it from what the product can tolerate, not from what the origin can survive.