stale-while-revalidate

Caching

A Cache-Control response directive (RFC 5861) that lets a cache keep serving a stale stored copy for a set number of seconds after max-age runs out while it revalidates in the background, so the visitor never waits on the origin. Caches that do not implement it ignore it and obey max-age alone.

10 min read Updated Aug 30, 2026

Full Explanation

stale-while-revalidate is a Cache-Control response directive. It gives a cache permission to keep serving a stored response for a set number of seconds after its max-age has run out, while the cache revalidates that response in the background. RFC 5861 introduces it as allowing a cache "to immediately return a stale response while it revalidates it in the background, thereby hiding latency (both in the network and on the server) from clients". It is not part of the core HTTP caching standard. It is an extension directive from an informational RFC. IANA registers it in the HTTP Cache Directive Registry. RFC 9111 section 5.2.3 tells caches to ignore extension directives they do not recognise. So a cache without support simply obeys max-age. It is not a freshness guarantee. It is not an availability mechanism. It is not something a client can ask for. RFC 5861 defines it for responses only. The request-side lever for accepting stale content is max-stale. Its mirror image is stale-if-error. That directive serves stale content only when the origin has failed. This directive serves stale content while the origin is perfectly healthy. The goal is purely to keep the refresh off the visitor's critical path. Varnish implements the same idea under the name grace mode.

How it works

  • The origin sends max-age plus a stale-while-revalidate value. RFC 5861 section 3 defines the syntax as stale-while-revalidate = "stale-while-revalidate" "=" delta-seconds. Delta-seconds in RFC 9111 is a non-negative integer. So fractional values are not valid.
  • The stored response is fresh while it is younger than its freshness lifetime. It is then served with no origin contact at all. The directive changes nothing until expiry.
  • The response can be stale but still inside the window. In that case, the cache MAY serve the stale content immediately. RFC 5861 says it SHOULD then "attempt to revalidate it while still serving stale responses (i.e., without blocking)". The visitor gets bytes at cache-hit speed. The origin fetch happens off to one side.
  • That background revalidation is a conditional request. Per RFC 9111 section 4.3.1 the cache attaches the validators it stored: an ETag in If-None-Match, or a Last-Modified date in If-Modified-Since. An unchanged origin answers 304 Not Modified. The cache then freshens the stored response from the 304's header fields (section 4.3.4) without re-transferring the body. If the resource changed, the 200 replaces the stored copy.
  • Nothing runs on a timer. Asynchronous validation happens only if a request arrives after the response went stale. The request must also arrive before the window ends. So the window size and your traffic rate together decide how often the trick works.
  • If the window passes without the entity being revalidated, RFC 5861 says it "SHOULD NOT continue to be served stale, absent other information". The copy is then "truly" stale. The next request blocks and is handled as a normal miss.
  • A response served this way is still visibly stale. It carries a non-zero Age larger than its freshness lifetime. RFC 5861 also expected a Warning header. But RFC 9111 section 5.5 obsoleted Warning and points at fields such as Age instead. So Age plus the cache's own status header is what you read in practice.

So Cache-Control: public, max-age=300, stale-while-revalidate=60 means this: the response is fresh for 300 seconds. Then it is servable stale for up to 60 more seconds while an asynchronous validation is attempted. RFC 5861's own worked example uses max-age=600, stale-while-revalidate=30. It spells out the consequence: "if validation is inconclusive, or if there is not traffic that triggers it, after 30 seconds the stale-while-revalidate function will cease to operate". The sizing advice follows from that. Treat max-age plus the window as the longest total time you can tolerate the response being served from cache. With both set to 600, that is 20 minutes.

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

Why it matters for a CDN

A CDN exists to keep the origin off the visitor's critical path. A miss puts it back. That means one round trip to the origin plus the origin's own processing time, all of it visible as TTFB. Without stale-while-revalidate, every expiry hands that bill to whichever visitor arrives first. With it, that visitor is served the stale copy at hit speed. The expensive refresh is paid by nobody.

  • It lets you keep a short TTL on volatile content, such as news, prices, or dashboards. You avoid paying an origin fetch on the visitor's request at every expiry.
  • It blunts the expiry burst. Cloudflare documents that all requests during the revalidation window are served from cache rather than waiting for the origin. So one origin fetch absorbs the whole burst instead of a cache stampede. Varnish coalesces concurrent requests for the same object. Grace hands the waiting ones stale content instead of queueing them behind the fetch.
  • It is bounded, not magic. The benefit holds only for requests that land inside the window. It also needs traffic dense enough to keep triggering refreshes. Beyond that, behaviour degrades to an ordinary blocking miss.

What CDNs do

  • Cloudflare only serves stale during revalidation if the origin's Cache-Control carries stale-while-revalidate: "without this directive, visitors wait for the origin to respond before receiving content". Revalidation is fully asynchronous. The first request after expiry triggers revalidation. It immediately receives stale content with an UPDATING status instead of blocking. The requests behind it do the same, until the origin responds. After that, requests get HIT. You can override an origin that sets the directive with the "Serve stale content while revalidating" setting in Cache Rules. Cloudflare documents two traps. First, with Always Online enabled, stale-while-revalidate and stale-if-error are ignored. Second, the value must be an integer. A floating-point value is ignored and can cause a cache bypass.
  • Fastly parses the value out of the Surrogate-Control or Cache-Control headers your origin sends. It stores that value in the beresp.stale_while_revalidate variable by default. You can set that variable in VCL instead. An expired object can be eligible for both stale-if-error and stale-while-revalidate. In that case, Fastly documents that stale-while-revalidate takes precedence.
  • Self-hosted caches implement the same pattern under their own names. Varnish serves a stale object while the sum of its TTL and grace is greater than zero. It performs a background fetch meanwhile. The Varnish Book notes that "the Cache-Control header also has the stale-while-revalidate directive to set the grace period". That is what the code does: RFC 5861's directive initialises grace. The default_grace parameter supplies 10 seconds when the backend sends nothing. beresp.keep is the companion knob. It keeps an expired object around so the background fetch can be conditional and take a 304.
  • nginx enables the same behaviour two ways. Since 1.11.10 it honours the header itself. The docs state that the "stale-while-revalidate" extension of Cache-Control "permits using a stale cached response if it is currently being updated". proxy_cache_use_stale updating does the same by configuration, at higher priority than the header. proxy_cache_background_update is off by default. It is what "allows starting a background subrequest to update an expired cache item, while a stale cached response is returned to the client".

In Varnish you set the grace period in VCL:

sub vcl_backend_response {
    # Allow serving stale for 60s while fetching fresh
    set beresp.grace = 60s;
    set beresp.ttl = 300s;
}

Watch out for

  • Directives that forbid stale serving win. RFC 9111 section 4.2.4 says a cache "MUST NOT generate a stale response if it is prohibited by an explicit in-protocol directive". It names no-cache, must-revalidate, and an applicable s-maxage or proxy-revalidate. s-maxage carries proxy-revalidate semantics for shared caches. So sending s-maxage alongside stale-while-revalidate silently kills stale serving at the CDN while leaving it alive in browsers. Cloudflare documents the same list and returns EXPIRED instead of UPDATING.
  • Targeted cache-control headers move the goalposts. If you send CDN-Cache-Control (RFC 9213 section 2.2), a cache that uses that field MUST ignore Cache-Control and Expires for the response. So a stale-while-revalidate left only in Cache-Control stops applying at that CDN. Repeat it in the targeted header. Note that RFC 9213 only requires such caches to implement max-age, must-revalidate, no-store, no-cache and private. Extension directives are a SHOULD.
  • It is not a freshness guarantee. RFC 5861 warns that "if the window is too small, or traffic is too sparse, some requests will fall outside of it, and block until the server can validate the cached response". So a quiet URL gets little benefit, however generous the window.
  • Browser caches honour it only if they implement it. MDN's compatibility data records support in Chrome 75, Firefox 68 and Safari 14. It records no support in Opera. Anything that lacks it silently ignores the value and uses max-age. Treat it as a shared-cache and CDN tool first.
  • It does nothing when the origin is erroring or unreachable. That is stale-if-error's job. The two are separate directives with separate windows. Neither covers for the other.
  • Refresh is driven by traffic on purpose. RFC 5861 section 5 suggests validation "be predicated upon an incoming request, to avoid the possibility of an amplification attack". So a cache that swept every expiring object would be the wrong implementation. That is why an idle URL never refreshes itself.

Best practice

  • Always pair it with max-age. The window is measured from the end of the freshness lifetime. Without a base freshness, there is nothing to be stale relative to.
  • Pick max-age plus window as the longest total staleness you can tolerate. Then split it. The max-age share sets how long you want the copy to be authoritative. The window share sets how much lateness you will trade for speed.
  • Size the window against the real request rate for that URL, not for the site. A 60-second window on a URL that sees one request a minute is a coin flip. On a hot URL, it is close to a guarantee.
  • Ship a validator. An ETag or Last-Modified on the origin response turns the background revalidation into a 304 with no body. That is what makes a short max-age cheap for the origin.
  • Short revalidation window, longer error window. Fastly's own guidance is exactly that: "set a short revalidation window, and a longer error window", with stale-if-error as the availability backstop.
  • To give the CDN a different window than browsers, do not reach for s-maxage. It disables stale serving in shared caches. Send a targeted CDN-Cache-Control header repeating the directives you want at the edge, or use a CDN-side control such as Cloudflare's Edge Cache TTL rule.
  • Verify it in production by reading the response. Look for a non-zero Age above max-age. Also look for a cache-status marker: Cloudflare's CF-Cache-Status: UPDATING, or whatever X-Cache-style status your CDN emits. That combination is the evidence that a stale hit was served while a refresh ran.

Examples

This is a common production header for content pages:

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

Content is fresh for 5 minutes. For 60 more seconds, the cache serves stale and fetches fresh in the background. If the origin is down, it serves stale for up to 24 hours.

In nginx, enable this with proxy_cache_use_stale:

location / {
    proxy_cache_use_stale updating;
    proxy_cache_background_update on;
    add_header Cache-Control "public, max-age=300, stale-while-revalidate=60";
}

Frequently Asked Questions

A Cache-Control response directive (RFC 5861) that lets a cache keep serving a stale stored copy for a set number of seconds after max-age runs out while it revalidates in the background, so the visitor never waits on the origin. Caches that do not implement it ignore it and obey max-age alone.

This is a common production header for content pages:

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

Content is fresh for 5 minutes. For 60 more seconds, the cache serves stale and fetches fresh in the background. If the origin is down, it serves stale for up to 24 hours.

In nginx, enable this with proxy_cache_use_stale:

location / {
    proxy_cache_use_stale updating;
    proxy_cache_background_update on;
    add_header Cache-Control "public, max-age=300, stale-while-revalidate=60";
}