Dated report · bounded stale serving · 25 Sep 2026
When stale HTTP content is allowed
A stale response is not automatically a bug or a guarantee. It is an allowed result only when storage, freshness, stale permission, and validation rules all fit the named cache and request.
The short answer: four gates
- Storage. Was this response allowed to be stored by this cache?
no-storeblocks ordinary HTTP-cache storage; unqualifiedprivateblocks a shared cache but can allow a private cache to store.stale-while-revalidateandstale-if-errordo not grant either permission. - Freshness. What lifetime applies at this cache? A shared cache uses
s-maxagebeforemax-ageandExpires; a private cache ignoress-maxageand uses its private-cache lifetime. A response is stale when its current age reaches the lifetime. - Stale permission. A supporting cache may use
stale-while-revalidatewhile it attempts validation, orstale-if-errorwhen the defined error path occurs, within the additional bound. These are permissions, not commands. - Validation. Without permitted stale reuse, a stale entry is conditionally validated or replaced.
If-None-Matchtakes precedence overIf-Modified-Since; a successful304 Not Modifiedlets the evaluating recipient reuse stored bytes, but does not identify the serving hop or prove application freshness.
Portable reading: RFC 9111's explicit prohibitions remain in force. Do not treat an extension as a portable override for response no-cache, must-revalidate, proxy-revalidate, or the shared-cache revalidation semantics carried by s-maxage. A different named-provider result needs that provider's documentation and an authorized test.
Fresh, stale, and validation states
| State at the named cache | Condition | Expected action | Not established by this state |
|---|---|---|---|
| Fresh | current_age < freshness_lifetime, with a matching key and Vary inputs | Reuse without validation unless a request directive such as no-cache asks for validation. | That a browser displayed it, another hop used it, or the entry remains resident. |
| Stale and revalidate | Stale, an applicable stale-while-revalidate window, and no stronger prohibition | Return old bytes while the cache should attempt non-blocking conditional validation. A 304 freshens; a 200 replaces. | That the background request happened, reached the origin, succeeded, or was visible to the client. |
| Stale on error | Stale, an applicable stale-if-error allowance, a defined error, and no stronger prohibition | Return old bytes instead of the error within the additional allowance, if the named cache supports the extension. | That the old 200 came from this extension unless the named implementation exposes the fallback path. |
| Ordinary stale rejection | No stale permission, an expired/unknown extension, or a prohibition | Validate if possible, use the representation after a 304, replace it with a full response, or propagate an error. must-revalidate requires an error rather than disconnected stale reuse. | That the old object was deleted; it may remain stored while validation is pending. |
Expected arithmetic example — unexecuted
Assume a public representation is stored by a private cache and a shared cache, the key and Vary inputs match, no header is rewritten, validators are correct, and no provider override exists. The private cache uses browser max-age=60. The shared cache uses s-maxage=300. This is arithmetic from the header values, not a live result.
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=120, stale-if-error=600 ETag: "rev-7" Last-Modified: Tue, 22 Sep 2026 14:00:00 GMT X-Origin-Revision: rev-7
| Selected current age | Browser/private cache: max-age=60 | Shared cache: s-maxage=300 | Permissive named implementation only | What remains unknown |
|---|---|---|---|---|
0–59 | Fresh; may reuse without validation. | Fresh; may reuse without validation. | Same fresh result. | Residency, storage at another layer, and what a client displayed. |
60 | Stale. A supporting private cache may serve old bytes in the candidate SWR window 60–180 while validating. | Fresh because 300 > 60. | Still fresh. | Support, request timing, and whether a background request ran. |
180 | Candidate SWR window ends at the boundary; ordinary validation is next unless another permission applies. | Fresh. | Fresh. | A provider may cap or decline the extension. |
299 | Stale and outside the candidate SWR window; validate. | Fresh. | Fresh. | Private and shared caches can be in different states for the same bytes. |
300 | Stale. A private SIE path can apply only if an error occurs before age 660. | Stale. Under the strict RFC 9111 reading, s-maxage carries shared revalidation semantics, so validate before reuse. | A named implementation could document an additional shared stale result; this is not portable from the header alone. | Which directive the provider applied and which hop selected the object. |
350 + 304 | A matching conditional result can let the evaluating cache reuse the stored body and update its metadata. | A shared implementation may have sent old bytes while validating and then freshened the object. | 304 does not identify the recipient as the origin or prove application state was current. | |
500 + 503 | Candidate private SIE can return old bytes through age 660, if supported and not prohibited. | Strict shared path does not use stale under the stated s-maxage semantics; it validates or exposes the error. | Candidate shared SIE through age 900 requires named documentation that changes the strict conflict result. | Error classification, provider configuration, and downstream copies. |
661+ | Private SIE allowance has ended; use the normal validation/error path. | No stale extension remains at age 901+ even under the permissive assumption. | Another stored object, TTL, key, or layer can still produce old bytes. | No live ages, statuses, or provider outcomes were observed. |
Boundary: RFC 9111 treats the response as stale when current age is equal to the freshness lifetime. “May” is not “will.” If no request arrives during a stale-while-revalidate window, RFC 5861 does not require a background validation to be scheduled.
Directive interaction matrix
These columns are intentionally separate. Storage permission, freshness, stale permission, and conditional validation answer different questions.
| Directive or validator | Storage permission | Freshness | Stale effect | Conditional validation |
|---|---|---|---|---|
max-age=N | Can support storage; it is not a private/shared safety decision. | Makes the response stale when current age reaches N; used by private caches and by shared caches when s-maxage is absent. | Does not itself permit stale reuse. | A stale entry can use its validators; max-age=0 is not no-store. |
s-maxage=N | Shared-cache freshness input and an Authorization reuse gate under RFC 9111; it does not make a user-specific body safe. | Overrides shared max-age and Expires; private caches ignore it. | Carries shared revalidation semantics. Do not present s-maxage plus stale-* as a portable stale guarantee. | Shared stale entries validate before reuse under the strict rule; 304 can freshen them. |
stale-while-revalidate=N | No storage permission; the response must already be storable. | Does not extend freshness. | A supporting cache may serve stale for the additional bound while it should attempt non-blocking validation. It does not override an explicit prohibition in the portable reading. | ETag/Last-Modified commonly make the background request conditional; 304 freshens and 200 replaces. |
stale-if-error=N | No storage permission; a stored response must exist to fall back to. | Does not extend freshness. | A supporting cache may use stale for a defined upstream/local error within the bound. A stale 200 alone does not prove this was the cause. | May follow a failed validation or next-hop error; error classification is implementation-dependent. |
no-cache (response) | Does not prohibit storage; it requires validation before reuse. | Freshness does not permit ordinary reuse without successful validation. | RFC 9111 says it prevents stale reuse, including by a cache configured to serve stale. Do not assume SWR cancels that rule. | Validators make the required validation cheap; a 304 allows reuse. |
no-store | Ordinary HTTP caches must not store the response or use it to satisfy another request. | No stored entry from this response has a freshness lifetime. | No new stale object is available. It does not erase an older entry or control service-worker, history, framework, or application copies. | Does not replace a prior entry or make the user agent forget other state. |
private | Unqualified form blocks shared-cache storage; a private cache may store. It is not encryption. | Uses the private cache's applicable max-age or Expires. | A private cache may apply a supported stale extension if no stronger prohibition applies; it never grants shared storage. | Private validators support browser-local validation; private does not itself require it. |
must-revalidate | Does not make a response cacheable; other storage rules still apply. It can participate in the Authorization shared-reuse gate. | Fresh entries may be reused. | Stale content must not be reused until successfully validated; if disconnected, generate an error rather than serve stale. | Conditional validation is the required path; a failed validation is not permission to use stale. |
proxy-revalidate | Does not make a response cacheable; it applies to shared caches. | No special lifetime. | A shared cache must validate stale before reuse. Private caches are not governed by this directive alone. | Same conditional path as must-revalidate for shared caches. |
public | Explicitly allows storage where a response might otherwise be restricted, subject to all other rules. It does not make personalized bytes safe. | No lifetime by itself; pair with an explicit lifetime. | Does not itself permit stale reuse. | Validators remain useful for mutable public content. |
ETag / If-None-Match | Validator presence grants neither storage nor freshness. | No lifetime. | No stale permission; it makes an allowed validation efficient. | Entity-tag condition is evaluated first when both request validators are present. For GET/HEAD, a matching condition can yield 304. |
Last-Modified / If-Modified-Since | Validator presence grants neither storage nor freshness. | No explicit lifetime; it can participate in a heuristic only where RFC 9111 permits one. | No stale permission. | Used for GET/HEAD when If-None-Match is absent and the date condition applies. It is less precise than an entity tag in some cases. |
304 Not Modified | Successful conditional result; a suitable stored response is reused and metadata can be updated. | Updates metadata under the recipient's validation rules; it is not a new response body. | Does not prove stale service before validation or prove a stale extension was used. | Confirms the evaluating recipient accepted the condition. A downstream cache may assemble a 200 from its stored body. |
For a full baseline on freshness, storage, key matching, and validators, use the report shelf and the ETag versus Last-Modified report. This follow-up narrows the stale path.
Symptom-first paths
Old content while the origin is healthy
- Define “old”: exact URL after redirects, method, query,
Varyinputs, redacted identity, body marker, browser/profile, and whether a request happened. History/bfcache or a service worker can show old content without an HTTP-cache request. - Capture
Date,Age,Cache-Control, validators,Vary,Via,Cache-Status, provider fields, request conditionals, and the exact body marker. - Interpret the named
Cache-Statusmember:hit; ttl=120is fresh reuse;fwd=stale; fwd-status=304supports stale-then-validated reuse;fwd-status=200supports a replacement fetch. None names every hidden hop. - Use
Ageas corroboration, not attribution. Correlate a unique origin request ID/counter if origin receipt matters.
Decision: positive named stale/revalidation evidence explains old bytes at that hop. Without it, classify the result as unknown.
Old content while the next hop errors
- Record whether the next hop returned 500/502/503/504, a timeout, connection failure, or DNS failure, and whether the client received a stale 200 or the error.
- Look for a named member such as
Edge; fwd=stale; fwd-status=503. It shows the forwarding/error sequence, not by itself thatstale-if-errorauthorized the final body. - Combine the response directives, named provider documentation/configuration, and final body. A
hit; ttl=-Ncan be consistent with stale fallback without forwarding, but it does not name the authorization reason. - Past the allowance, or with
no-cache,must-revalidate, applicables-maxage, orproxy-revalidate, investigate a provider override, another layer, a mismatched key, or an application/service-worker response.
Decision: call it expected only at a named hop with the matching error and documented allowance. Do not call an unscoped old 200 “stale-if-error.”
Old content while revalidation is in progress
- Establish that the named cache was stale, the
stale-while-revalidatewindow applied, and no stronger prohibition blocked stale reuse. - Capture the conditional request. If both validators are present,
If-None-Matchwins. A 304 keeps the stored representation; a full 200 should replace it under the cache rules. fwd=stale; fwd-status=304supports old bytes while unchanged validation runs;fwd-status=200supports old bytes while a replacement was fetched.collapsedcan show a shared forward, but its absence proves nothing about an unreported layer.- If old content persists after the window or replacement, inspect browser cache, service worker/Cache API, history/bfcache, another proxy, redirect target, query/variant, and response-header rewriting.
Decision: a 304 proves conditional equivalence at its recipient, not application freshness or global update.
Named-hop evidence and its limits
| Signal | What a positive value can support at the named hop | What it cannot prove; what absence means |
|---|---|---|
Age | RFC 9111 defines a sender estimate of time since generation or successful origin validation. A nonzero value supports a stored-response path at some cache. | It does not identify the inserting cache, POP/tier, key, stale permission, or complete chain. Age: 0 or missing Age does not prove origin contact; another layer may omit or rewrite it. |
RFC 9211 Cache-Status | Each member names one cache. hit means that member satisfied the request without forwarding; fwd=stale means it selected stale and forwarded; fwd-status=304/200/503 describes the next-hop result; negative ttl indicates calculated staleness. | It is optional. Missing/stripped members do not mean no cache. hit does not state why stale was allowed; fwd=stale does not prove the next hop was the origin. Vendor detail values are local. |
Via | A visible member identifies a proxy or gateway that forwarded the message and can corroborate a named intermediary. | It is not hit/miss or freshness evidence. Intermediaries can combine, hide, or omit visible layers. Missing Via does not prove a direct origin connection. |
| Provider fields | A documented CloudFront or Cloudflare field, POP identifier, CF-Cache-Status, X-Cache, or configured Server-Timing can report that named product's decision for that request. | Names and values are product/configuration-specific and may be absent, rewritten, sampled, or emitted by another tier. A positive field does not identify browser/service-worker state or global eviction; absence does not prove miss or origin contact. |
304 Not Modified | The recipient accepted a conditional request and may reuse/update its stored representation. RFC 9110 defines a no-content response for conditional GET/HEAD. | It does not identify origin versus intermediary, prove application freshness, prove stale service before validation, or guarantee the client saw 304. A cache can return a 200 assembled from stored bytes. |
| Origin revision marker | A marker identifies the representation revision delivered at the observed hop. A unique request ID correlated with an origin log proves that instrumented origin received that request. | A stable marker can be cached. No counter increment can mean a cache hit, 304 body path, stripped marker, another origin, or limited logging. Absence is unknown. |
RFC 5861 mentions Age and a warning field for stale responses; RFC 9111 later obsoletes the Warning field because it was not widely generated or surfaced. Do not expect a missing warning to disprove stale use.
Safe policy guardrails
Personalized or sensitive responses
- Use
no-storewhen intentional HTTP-cache retention is unacceptable. Review service-worker, framework, application, history, and provider storage separately;no-storeis not a deletion command or complete confidentiality guarantee. - If browser retention with validation is acceptable, use a deliberate private policy such as
private, no-cachewith a validator. Do not add stale extensions unless the private-cache risk is explicitly acceptable. - Cookie,
Authorization, orSet-Cookiealone is not a privacy guarantee. Review URI, method, query,Vary, identity, language/device/geo, redirects, worker, and application keys. - A short stale window limits neither a bad shared key nor exposure. Never use stale directives to make a user-specific body public.
Public dynamic content
- Use explicit browser/shared lifetimes and validators. Decide how old content may be and whether serving it during origin errors is acceptable.
- Keep
max-ageands-maxageseparate. Do not advertise a shared stale period froms-maxage + stale-*without named-cache documentation or an authorized test. - If each reuse needs origin confirmation, use response
no-cachewith validators. Do not assume SWR creates an exception.
Versioning versus purge
| Need | Prefer | Boundary |
|---|---|---|
| Routine JS/CSS/image/font release | New versioned or content-hash URL, then update HTML and manifests. | The old URL can remain in browser, service-worker, corporate-proxy, or provider storage. |
| Mutable HTML at a stable URL | Short explicit freshness plus validators; use a narrow named-provider purge only for an urgent stable-object change. | Purging one provider does not clear browser history, service workers, other proxies, or other edges. |
| Emergency correction of a known provider object | Authorized exact URL/path/tag/prefix purge or invalidation, then verify from a named vantage. | Receipt or completion proves the named operation, not universal eviction. |
| Private-response exposure | Correct representation policy and key first; contain each named layer; purge known provider copies second. | No purge proves that no browser, worker, proxy, log, or other layer has a copy. |
Use the provider-neutral deployment and incident runbook for the broader representation, key, downstream, release, and containment sequence. This report does not repeat the CloudFront or Cloudflare controls; see their CloudFront report and Cloudflare report for named boundaries.
Authorized-only test matrix
This is a copyable plan for a sanitized endpoint such as https://AUTHORIZED.example.test/probe. The URL, marker, timings, responses, provider fields, and commands are placeholders. No row was executed. Do not use production URLs or real credentials.
Capture contract for every row: UTC start/end; exact URL after redirects; method/status; request and response headers; response length and body marker; request Cache-Control, validators, redacted cookie/auth presence, and Vary inputs; response Date, Age, Cache-Control, validators, Via, Cache-Status, and named-provider fields; origin request ID/counter including 304s; and, only if authorized, browser Network/worker/history state.
URL='https://AUTHORIZED.example.test/probe' # Illustrative shape only; not executed. curl -sS -D /tmp/probe.headers -o /tmp/probe.body "$URL?case=CASE_ID"
| Case | Fixture | Required timing/error | Expected outcome if the named cache supports it | Compare | Claim ledger |
|---|---|---|---|---|---|
| Fresh baseline | public, max-age=60, s-maxage=300; ETag: "a"; body rev-a | GET, then repeat below both lifetimes. | Private/shared reuse without validation; no origin increment for a true named hit. | Age, Cache-Status: ...; hit, provider field, marker, origin log. | Expected; unexecuted. |
| Private SWR window | public, max-age=5, stale-while-revalidate=20; ETag: "a" | Populate; wait at least 6 seconds; issue two serialized GETs. | A supporting cache may return rev-a and run an asynchronous conditional request; 304 freshens or 200 replaces. | Conditional request, status/body, Cache-Status, origin timing. | Normative extension + expected implementation; unexecuted. |
Shared s-maxage conflict | public, max-age=5, s-maxage=10, stale-while-revalidate=20, stale-if-error=30 | Test ages 6, 11, 15 with healthy origin, then induced 503. | Private uses 5; strict shared reading validates at age 10. Shared stale, if observed, needs provider documentation/configuration. | Effective TTL, Cache-Status, provider config, counter, marker. | Normative conflict boundary; result unknown/unexecuted. |
no-cache versus SWR | public, no-cache, stale-while-revalidate=20; ETag: "a" | Repeat while fresh and stale. | Stored entry can be retained, but reuse requires successful validation; do not assume stale bypass. | Request conditionals, 304/200, fwd, Age, origin log. | Normative expected; unexecuted. |
no-store versus old entry | First store rev-a; then response sends no-store. | Change origin; repeat same URL from a client with the old entry. | New response is not stored; old copies are not erased by the directive. | Network existence, body marker, storage owner, worker/history state. | Normative/browser boundary; unexecuted. |
| Must revalidate on outage | public, max-age=5, must-revalidate; ETag: "a" | Populate; wait past 5; disconnect origin or return 503. | No stale reuse; validation or an error, normally 504 when disconnected. | Status, Cache-Status, fwd-status, failure log, body. | Normative expected; unexecuted. |
| SIE within/outside window | public, max-age=5, stale-if-error=30; no shared revalidation prohibition. | Return 503 at ages 6, 34, and 36. | A supporting cache may return stale 200 through the additional window; after the bound, the error is written through. | fwd=stale; fwd-status=503 or local fallback field, final status, age/ttl, error log. | Expected implementation; unexecuted. |
| Validator precedence | Stored ETag: "a", Last-Modified: T1; request both conditions. | Repeat with matching ETag, then nonmatching ETag while date remains T1. | Matching ETag can yield 304; a nonmatching ETag prevents date fallback. | Raw conditionals, status/body, origin validator log. | Normative; unexecuted. |
| Named-hop chain | Any public fixture with a known stale/revalidation event. | Capture client, named provider, and controlled origin; preserve repeated Cache-Status members. | Interpret hit, fwd=stale, fwd-status, ttl, stored, and collapsed per member; Via is forwarding evidence only. | Raw headers, provider trace, origin ID/counter. | Evidence semantics; no observation. |
Claim ledger
| Claim | Label | Evidence required | Status here |
|---|---|---|---|
| RFC 5861 defines bounded stale-while-revalidate and stale-if-error response extensions. | Normative | RFC 5861 §§3–4. | Confirmed from current source text. |
no-cache, must-revalidate, proxy-revalidate, and shared s-maxage impose validation constraints before stale reuse under the portable RFC 9111 reading. | Normative | RFC 9111 §§4.2.4 and 5.2.2. | Confirmed from current source text; named implementation precedence remains a separate question. |
| A cache will implement the extension and serve old content in the stated window. | Expected | Named-cache documentation plus an authorized timing/error capture. | Not tested. |
| A particular browser HTTP cache honors response stale extensions for a navigation. | Unknown | Browser/version, request path, Network record, and controlled repeat. | No browser session or capture. |
| A CloudFront or Cloudflare request was stale-served, revalidated, collapsed, or purged. | Unknown | Named provider trace/configuration, raw response, and origin correlation. | No provider account, request, or result; existing provider reports are documentation-only. |
Age: N proves that a named cache served stale. | Not supported | Header attribution plus named Cache-Status/provider trace. | Age alone does not identify stale permission or the inserting cache. |
Cache-Status: Edge; fwd=stale; fwd-status=304 identifies stale validation at Edge. | Evidence interpretation | Raw header from an authorized request. | No such header was observed; interpretation follows RFC 9211. |
| A body revision marker proves the origin handled the current request. | Unknown unless correlated | Unique request ID/counter, origin log, and preserved response correlation. | No controlled origin or marker capture. |
| A 304 means the application state was current. | Not supported | Application-level version/freshness evidence in addition to validator evidence. | RFC 9110 establishes conditional equivalence at the evaluating recipient only. |
Missing Age, Cache-Status, Via, provider field, or origin marker proves no cache or no origin contact. | Not supported | Complete topology and logs. | Absence remains unknown. |
Sources and uncertainty
Direct source pages were retrieved or cross-checked 25 Sep 2026 during 06:10:35–06:14:41 UTC. RFCs are the primary authority; MDN and Chrome documentation provide browser-facing interpretation. No live URL or response was tested.
| Source | Use in this follow-up | Boundary |
|---|---|---|
| RFC 9111 · HTTP Caching | Storage, freshness, current age, stale constraints, validation, Age, no-cache, no-store, private, must-revalidate, proxy-revalidate, and s-maxage. | Normative cache semantics; it does not identify a named CDN's configuration or implementation result. |
| RFC 5861 · stale controls | §§3–4 bounded stale-while-revalidate and stale-if-error permissions and examples. | Informational extension; it does not require every browser, CDN, proxy, or application cache to implement the extensions. |
| RFC 9110 · HTTP Semantics | Validators, conditional precedence, 304, and Via. | Conditional equivalence at the evaluating recipient is not application freshness or origin attribution. |
| RFC 9211 · Cache-Status | Optional named-cache members such as hit, fwd=stale, fwd-status, ttl, and collapsed. | Optional, scoped diagnostics; omission is not a negative result and each member describes only its named cache. |
| MDN · Cache-Control and MDN · HTTP caching | Developer-facing directive wording, browser/shared distinction, versioning, and browser/history limits. | MDN does not establish this pool's browser or provider behavior; RFCs remain normative. |
| MDN · conditional requests | Browser-facing ETag/Last-Modified and 304 explanation. | Does not prove a particular browser or intermediary generated a given 304. |
| Chrome DevTools · Network and Network reference | Header, timing, initiator, request-source, preserve-log, and Disable cache inspection guidance. | Chrome-specific procedure; disabling browser cache changes the experiment and does not bypass every service worker or intermediary. |
| Existing provider-neutral runbook | Broader deployment, key, downstream, marker, and purge boundaries. | Link used for continuity; this report does not repeat the runbook. |
| Existing CloudFront report and existing Cloudflare report | Named-provider TTL, key, response-field, stale/revalidation, and purge boundaries. | Provider-specific documentation-only material; no current distribution, zone, POP, trace, or response was available. |
| Existing ETag versus Last-Modified report | Prior validator precedence and 304 boundary. | Linked rather than repeated; no new validator probe was run. |
Uncertainty boundaries
- Implementation support, stale-window caps, error classification, request collapse, and directive precedence at a named cache remain untested.
- Browser HTTP cache, history/bfcache, service-worker fetch, Cache API, framework cache, and application state are separate evidence domains. The HTTP response alone cannot attribute them.
Age,Cache-Status,Via, provider fields, 304, and origin markers are partial observations. Missing fields are unknown.- Versioned URLs create new targets; they do not delete old copies. A named purge or invalidation affects only its documented scope.
- No authorized URL, request, response, browser capture, provider trace,
Age,Cache-Status,Via, 304, origin marker, origin counter, purge, invalidation, or analytics result was produced here. All examples and timing rows are explicitly unexecuted.