Freshness vs. speed: the stale-while-revalidate cache strategy

Serving content that is both immediate and up to date is a constant balancing act. The HTTP stale-while-revalidate caching directive offers a way to ease the tension by defining a window in which stale content can be served immediately while the cache is refreshed in the background. Support for this directive in the Cache-Control response header ships in Chrome 75 and Firefox 68. Browsers without support simply ignore the directive and stick to standard max-age semantics.

The directive is designed for scenarios where an asset or API response changes on a predictable schedule, where staleness for a short period is acceptable, and where you want to avoid having every request block on the network. It pairs specifically with max-age in the Cache-Control header.

How time-based revalidation works

Decoding stale-while-revalidate comes down to two age checks, both driven by max-age and the stale-while-revalidate window.

The first check is the familiar freshness check: if the cached response is not yet max-age seconds old, it is considered fresh and is served directly with no additional work. The interesting behavior appears only after a response goes stale.

Once a response is older than max-age, its age is compared against the extension window offered by stale-while-revalidate:

  • Populated within the window: If the response's age is beyond max-age but still within the stale-while-revalidate period, the stale copy fulfills the current request without delaying it. Meanwhile, a background revalidation request goes to the network. Its result—which may or may not differ from the previous body—replaces the cached copy and resets the freshness timer for future requests.
  • Outside the window: If the cached response is older than the combined total of max-age plus stale-while-revalidate, the browser ignores the cache entirely and obtains the response from the network, using that new response for both the current request and to populate the cache.

Consider a time API that reports the number of minutes past the hour, served with the following header:

Cache-Control: max-age=1, stale-while-revalidate=59

This means roughly:

  • Repeated requests within 1 second hit a perfectly fresh cache, and no revalidation occurs.
  • Requests arriving 1 to 60 seconds after the last response fulfill the API call with the stale copy while a background request refreshes the cache.
  • Requests arriving after 60 seconds miss the reuse window and must wait for a full network round-trip.

A diagram illustrating the information from the previous section.

Use cases for the directive

But the idea isn't limited to contrived examples. Practical targets include weather API feeds and hourly news headlines—anything that updates on a known cadence, sees repeated requests, and remains valid and unchanged between updates. Short-term caching with max-age was already the go-to option for such data; adding stale-while-revalidate makes the fallback substantially better by serving from the cache in the stale period and avoiding a blocking network trip.

Delivering a fresh-first version of stale content

For API users, the benefit shows up first in later requests: if an earlier network call refreshed the cache during the stale window, then everyone else is now getting a fresh answer. For products that need to handle traffic spikes, this helps smooth out bursty loads.

Data that doesn't change much yet is highly requested (e.g., a database drive that updates once a day) carries the same benefit. The pattern adds almost no client-side complexity.

Service worker vs. header integration

Many developers will recognize stale-while-revalidate from common service worker recipes. The header approach and the service worker version share the same conceptual goal, and similar freshness / maximum-lifetime trade-offs apply. The implementation should be chosen deliberately, because the two interact in a defined order: if your site runs a service worker, it generally gets the first chance to answer a request. Failing that, the service worker may generate a response by making a network request with fetch(). In that case, the Cache-Control header behavior takes over.

The header approach suits many cases; the service worker approach suits others. Here's where each plays best:

When a service worker is the right fit

  • Your app already operates via a service worker for offline-first or other reasons.
  • You need granular control over exactly which URLs or requests are cached, plus logic to prune your caches—for example an least-recently-used expiration policy, optionally with Workbox’s Cache Expiration module.
  • You need to know when the background revalidation has resulted in an update to the live content, as implemented in Workbox’s Broadcast Cache Update module.
  • You must support this exact caching pattern in every modern browser, since the workbox implementation covers all of them — the header alone doesn't work everywhere yet.

When a Cache-Control header approach works fine

  • You don't want the operational overhead of keying up and maintaining a service worker.
  • You accept the browser’s automatic cache-eviction strategies to bound cache size.
  • You accept user agents without support for the directive will use their standard (non-stale) behaviors temporarily and fall back to max-age.

Know the current limits

As of publication, header-based stale-while-revalidate isn't universal – Chrome 75+ and Firefox 68+ implement it, but other active engines (notably Safari) don't. Treating the header as progressive enhancement—a fallback to max-age—keeps your app correct everywhere without weakening the signature. Broader support may come over time, per the relevant specifications.