Stale Content

Caching

Stale content is a cached response whose freshness lifetime has passed. RFC 9111 forbids serving it unless the cache is disconnected or the client or origin permits it — which is what stale-while-revalidate and stale-if-error do, making the expired copy a resilience resource. A backstop, not a bug.

Also known as stale response, stale object.

14 min read Updated Aug 30, 2026

Full Explanation

Stale content is a cached response whose freshness lifetime has passed. Its age has overtaken the TTL the origin set. A cache may then no longer hand it out unconditionally. RFC 9111 section 4.2 (STD 98, June 2022, obsoleting RFC 7234) states it as arithmetic, not as a flag: “A ‘fresh’ response is one whose age has not yet exceeded its freshness lifetime. Conversely, a ‘stale’ response is one where it has.”

What it is not: staleness is not corruption, not an error, not eviction. The bytes are intact and still stored. Expiry marks the copy. It does not delete it. Stale does not mean unusable either. RFC 9111 section 4.2.4 forbids serving a stale response by default. It then lists the ways that prohibition is lifted. Two Cache-Control extension directives from RFC 5861 exist precisely to lift it: stale-while-revalidate and stale-if-error. Used deliberately, an expired copy is the most valuable thing a CDN holds when the origin is slow or dead. It is a backstop, not a bug.

How it works

A cache compares the stored response’s age against the freshness lifetime the origin declared. It normally does this with max-age, or s-maxage for a shared cache, or an Expires header. While age stays below the lifetime, the response is fresh. It “can be used to satisfy subsequent requests without contacting the origin server”. Once age passes the lifetime, the same bytes become stale (RFC 9111 section 4.2). Nothing else about the object changes.

Stale is then a permission question. Section 4.2.4 states the default prohibition: “A cache MUST NOT generate a stale response unless it is disconnected or doing so is explicitly permitted by the client or origin server”. Note who is disconnected: it is the cache, not the origin. The same section names four routes through that door:

  • The cache is disconnected, so it has nothing fresher to offer.
  • The client asks, with the max-stale request directive stating how much staleness it accepts.
  • The origin permits it, with an extension directive. In practice, these are the two from RFC 5861.
  • An out-of-band contract configures it. This is how most CDN dashboards do it: the setting, not the header, grants the permission.

The two RFC 5861 directives split the work by which failure they cover.

  • stale-while-revalidate=seconds hides latency. Caches “MAY serve the response in which it appears after it becomes stale, up to the indicated number of seconds”. A cache that does so “SHOULD attempt to revalidate it while still serving stale responses (i.e., without blocking)” (RFC 5861 section 3). The client is answered from cache. The refresh happens behind it. Both are permissions, not obligations. That is why the same header produces different behaviour on different platforms.
  • stale-if-error=seconds hides failure. It applies when “an error is encountered, a cached stale response MAY be used to satisfy the request, regardless of other freshness information”. Here, an error is defined as “any situation that would result in a 500, 502, 503, or 504 HTTP response status code being returned”. The value is “the upper limit to staleness”. Past that value, the copy “SHOULD NOT be used to satisfy the request, absent other information” (RFC 5861 section 4). The worked example in section 4.1 shows the arithmetic. With max-age=600 and stale-if-error=1200, once age exceeds 1800 seconds, “the cache must write the error message through”.

If no permission applies, the cache falls back to revalidation. It sends a conditional request carrying a validator it stored: an ETag in If-None-Match, or a Last-Modified date in If-Modified-Since, and “this process is known as ‘validating’ or ‘revalidating’ the stored response” (RFC 9111 section 4.3). A 304 Not Modified freshens the stored copy without retransmitting the body. That is why staleness is cheap to resolve when a validator is present, and expensive when it is not.

A stale hit is visible on the wire. A cache that satisfies a request from store without validating “MUST generate an Age header field” (RFC 9111 section 4). So the response arrives with an Age past the freshness lifetime. RFC 5861 section 3 also expected a Warning header. But RFC 9111 section 5.5 has since obsoleted Warning. What it carried “can be gleaned from examining other header fields, such as Age”. That leaves Age as the only standard staleness signal left.

Why it matters for a CDN

  • The edge is the last cache before the user. Its stale copy is the last line of defence. When the origin cannot answer, the only alternatives are the expired object or an error page. Fastly states the trade-off exactly. Set a short revalidation window and a longer error window. That is “because while you may not want to serve out of date content for very long, if the origin server is down, it’s either that or an error message” (Fastly, Serving stale content).
  • It moves origin latency off the request path. With stale-while-revalidate, the round trip leaves the served path. The user gets the stored copy immediately, and the refresh completes for the next visitor. Cloudflare documents revalidation as fully asynchronous. The first request after expiry “triggers revalidation in the background” and “immediately receives stale content with an UPDATING status instead of blocking until the origin responds” (Cloudflare, Revalidation).
  • It absorbs the load spike that expiry creates. Without it, a popular object expiring releases every queued request at the origin at once. Varnish calls this the “thundering herd problem”. It gives that as a reason to serve stale during the fetch: “suddenly releasing a thousand threads to serve content might send the load sky high” (Varnish, Grace mode and keep).
  • The cost is bounded staleness, not unbounded risk. Some users receive a response a few seconds old, for a window you choose. That makes it a resilience tool rather than a freshness tool. It belongs on content whose disappearance is worse than its age. It never belongs on a price, a balance, or a stock count.

What CDNs do

A stale directive is a request, not a contract. What happens depends on the vendor, on the plan or cache interface, and on any sibling directive in the same header. Behaviour from each vendor’s current documentation:

  • Cloudflare honours origin Cache-Control through Origin Cache Control. This is enabled by default for Free, Pro and Business customers, who cannot disable it. Enterprise customers select it through cache rules instead. Its stale-while-revalidate is fully asynchronous: the first post-expiry request and every request during revalidation are served stale with an UPDATING status, then a HIT once the origin answers. stale-if-error “triggers when your origin returns a 5xx status code (500, 502, 503, 504)”. Other responses “such as 404 are not considered errors for this purpose — the origin response replaces the stale cached content”. There is also “currently no way to customize which status codes trigger stale-if-error” (Cloudflare, Cache-Control directives).
  • Fastly implements RFC 5861. How much of it you get depends on the cache interface. The readthrough interface has “full native HTTP caching semantics”. The core interface “supports manual SWR mechanics, but does not natively manage SIE”. The simple interface “has no concept of staleness or revalidation, and cannot track or serve expired objects”. Inside a stale-while-revalidate window an object “will be served immediately to the client” and an asynchronous backend fetch is scheduled. stale-if-error is narrower than it looks. Stale is served automatically only when the origin is sick, meaning “currently failing a health check”. An erroring origin (a syntactically valid 5xx) or one that is down or unreachable only sets stale.exists instead. The stale copy “may be used by explicitly selecting it” in VCL. Fastly also warns that “unlike SWR, SIE doesn’t allow for any asynchronous revalidation”: with stale-if-error alone, the first request after expiry blocks on a synchronous fetch (Fastly, Lifetime and revalidation).
  • Akamai drives stale serving from property configuration rather than the RFC 5861 directives. Its Caching behaviour page does not document those directives. The choice is explicit. The “Serve stale if unable to validate” option “serves a stale object when the origin can’t be reached or returns a 5xx error”. It resets the object’s TTL, and it “doesn’t apply to other response codes”. The “Always revalidate with origin” option instead “blocks the serving of a stale object” in those cases, and returns a 504 Gateway Timeout. Either way Akamai tries the origin first: “before serving a stale object to a user, Akamai will attempt to revalidate the stale object by making a ‘revalidation request’ to the origin”. Under the Honor origin Cache-Control options, a must-revalidate or proxy-revalidate directive makes edge servers “ignore the Force revalidation of stale objects setting”. With Enhanced RFC support, “stale objects are not served if s-maxage is present” (Akamai, Caching).
  • Varnish calls it grace. Setting an object’s grace “tells Varnish that it should serve the object to clients for some time after the TTL has expired, while Varnish fetches a new version of the object”, with the default set by the default_grace runtime parameter. A separate keep value holds the object longer purely as a 304 candidate, and the two are additive. The documented idiom is to cap grace through req.grace while the backend is healthy, and let a long grace apply only when it is sick: stale-while-revalidate and stale-if-error on one knob. In current Varnish the builtin vcl_hit needs no obj.ttl + obj.grace > 0s test, because that condition “will (in sub vcl_hit) always evaluate to true”. In “6.0.0 and earlier” the test was necessary to stop keep-only objects reaching clients (Varnish).
  • nginx serves nothing stale until told to: proxy_cache_use_stale defaults to off. Its trigger list is wider than RFC 5861’s: error, timeout, invalid_header, updating, http_500, http_502, http_503, http_504, http_403, http_404 and http_429. Updating permits reuse of a stale response “if it is currently being updated”. nginx does read stale-while-revalidate and stale-if-error from the response, but that path “has lower priority than using the directive parameters”. The configuration wins over the header (nginx, ngx_http_proxy_module).

Watch out for

  • A sibling directive cancels the whole thing, silently. “A cache MUST NOT generate a stale response if it is prohibited by an explicit in-protocol directive (e.g., by a no-cache response directive, a must-revalidate response directive, or an applicable s-maxage or proxy-revalidate response directive)” (RFC 9111 section 4.2.4). Cloudflare lists the same four and warns that with any of them present “Cloudflare will not serve stale content — requests will return EXPIRED instead of UPDATING”. Note applicable: s-maxage and proxy-revalidate bind shared caches. So they break stale serving at the CDN while leaving a browser’s private cache untouched.
  • s-maxage is the usual accident. Reaching for it to give the edge a different lifetime from the browser also imports proxy-revalidate semantics and kills stale-while-revalidate. Cloudflare’s documented workaround is to “not use s-maxage”: send max-age together with stale-while-revalidate and set the edge lifetime with Edge Cache TTL in Cache Rules instead (Cloudflare, Revalidation).
  • must-revalidate is absolute, outage included. “In all circumstances, a cache MUST NOT ignore the must-revalidate directive; in particular, if a cache is disconnected, the cache MUST generate an error response rather than reuse the stale response”. The status that results “SHOULD be 504 (Gateway Timeout)” (RFC 9111 section 5.2.2.2). It beats stale-if-error and it beats the disconnected exception. So use it only where a wrong answer is worse than no answer.
  • A window too short for your traffic does nothing. Asynchronous revalidation happens only if a request lands after expiry and before the window closes. 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”. After that, the function “will cease to operate” (RFC 5861 section 3.1). Size the window against the real gap between requests for that object, not against a round number.
  • stale-if-error only covers server failure. A 404, 403 or 410 is not an error for this purpose. On Cloudflare that origin response “replaces the stale cached content”. So a bad deploy answering 404 overwrites the good copy you were relying on, instead of falling back to it. Past the directive’s value, the real error is written through.
  • There is nothing to serve if the object was evicted. Fastly is explicit that content “is not guaranteed to be stored for the entire freshness lifetime”. It also says that large, infrequently requested objects “may be evicted to make space for more popular objects”. A long stale-if-error window is a permission, not storage. Long-tail URLs are precisely the ones that will be missing when the origin fails.
  • Background refresh can be turned into an amplifier. RFC 5861 suggests “that such validation be predicated upon an incoming request, to avoid the possibility of an amplification attack” (RFC 5861 section 5). Never drive revalidation from a timer across a large URL set.
  • A shield can re-cache stale as fresh. With an origin shield in front of the origin, the edge normally declines to store a stale response, because the shield’s Age already exceeds the lifetime. Fastly documents soft purge as the exception: the shield marks its copy stale and keeps serving it. An edge POP requesting it during that window “may misinterpret the response headers and cache the stale data with a fresh TTL”.
  • Do not extend stale serving to private responses. A shared cache “MUST NOT use a cached response to a request with an Authorization header field”. This applies to any subsequent request, unless a response directive allows shared storage: must-revalidate, public or s-maxage (RFC 9111 section 3.5). Cookie-based sessions get no such protection. Set-Cookie “does not inhibit caching”. Deployment flaws “might lead to the caching of sensitive information (e.g., authentication credentials) that is thought to be private, exposing it to unauthorized parties” (RFC 9111 section 7.3). A long stale window only widens that blast radius.

Best practice

  • Send both directives, sized differently: a short stale-while-revalidate that covers the normal gap between requests, and a much longer stale-if-error. Fastly’s serving-stale tutorial illustrates the shape with 60 seconds and 86,400 seconds (Fastly).
  • Always ship a validator. With an ETag or Last-Modified, Fastly performs “conditional revalidation” and a 304 resets the object’s lifetime. Without one it falls back to “unconditional revalidation” and refetches the whole body (Fastly).
  • Budget the total, not just the TTL. RFC 5861 notes that servers will generally want to set “the combination of max-age and stale-while-revalidate to the longest total potential freshness lifetime that they can tolerate” (RFC 5861 section 3.1). With both at 600, the origin must tolerate a 20-minute-old response.
  • Audit the rest of the Cache-Control line before blaming the edge. no-cache, must-revalidate, s-maxage or proxy-revalidate in the same header disables stale serving, whatever the stale directives say.
  • Prove it rather than assume it. Request the object again past expiry and read Age together with the vendor status header: X-Cache or an equivalent elsewhere, CF-Cache-Status on Cloudflare. The three values tell you which mechanism fired: UPDATING is “expired but served from Cloudflare’s cache while the origin updates it in the background”. STALE is “served from Cloudflare’s cache but was expired”, because “Cloudflare could not contact the origin”. EXPIRED or REVALIDATED means no stale copy was served at all. REVALIDATED appears precisely when “stale-while-revalidate is not set” or a directive such as must-revalidate or no-cache “prevent stale content from being served” (Cloudflare, Cloudflare cache responses).
  • Keep stale serving off sessions, authentication, carts, prices and anything a user can act on or be billed for. Pair long stale windows with a working purge path, so a bad copy can be removed on demand rather than waited out.

Examples

# Resilient caching with stale directives
Cache-Control: public, max-age=300, stale-while-revalidate=60, stale-if-error=86400
# Fresh for 5 min, serve stale for 1 min while revalidating,
# serve stale for 24h if origin is down

# Varnish: grace mode
sub vcl_backend_response {
    set beresp.grace = 1h;
}
sub vcl_hit {
    if (obj.ttl + obj.grace > 0s) {
        return (deliver);  # Stale but within grace
    }
}

# Nginx: proxy_cache_use_stale
proxy_cache_use_stale error timeout updating http_500 http_502 http_503;

Frequently Asked Questions

Stale content is a cached response whose freshness lifetime has passed. RFC 9111 forbids serving it unless the cache is disconnected or the client or origin permits it — which is what stale-while-revalidate and stale-if-error do, making the expired copy a resilience resource. A backstop, not a bug.

# Resilient caching with stale directives
Cache-Control: public, max-age=300, stale-while-revalidate=60, stale-if-error=86400
# Fresh for 5 min, serve stale for 1 min while revalidating,
# serve stale for 24h if origin is down

# Varnish: grace mode
sub vcl_backend_response {
    set beresp.grace = 1h;
}
sub vcl_hit {
    if (obj.ttl + obj.grace > 0s) {
        return (deliver);  # Stale but within grace
    }
}

# Nginx: proxy_cache_use_stale
proxy_cache_use_stale error timeout updating http_500 http_502 http_503;

Yes. Stale Content is also known as stale response, stale object. Stale content is a cached response whose freshness lifetime has passed. RFC 9111 forbids serving it unless the cache is disconnected or the client or origin permits it — which is what stale-while-revalidate and stale-if-error do, making the expired copy a resilience resource. A backstop, not a bug.

Related CDN concepts include:

  • must-revalidate — must-revalidate is a Cache-Control response directive (RFC 9111): once a stored response goes stale, any …
  • stale-if-error — stale-if-error is a Cache-Control extension directive (RFC 5861) that lets a cache serve an already-stored …
  • stale-while-revalidate — A Cache-Control response directive (RFC 5861) that lets a cache keep serving a stale stored …
  • Cache-Control — Cache-Control is the HTTP header field, defined in RFC 9111, that carries caching directives to …
  • PURGE — PURGE is an operator- or API-triggered invalidation that removes cached objects from a CDN edge …
  • TTL (Time To Live) (TTL) — TTL (time to live) is how many seconds a cached response stays fresh before a …