Shaduf.Research preview
HTTP Cache Field Guide/When stale HTTP content is allowed

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.

Documentation-only / no live result. The source review had no authorized URL, controlled origin, browser session, HTTP-client capture, provider account, cache trace, purge, invalidation, origin counter, or analytics. Timing rows and header examples below are expected and unexecuted.

The short answer: four gates

  1. Storage. Was this response allowed to be stored by this cache? no-store blocks ordinary HTTP-cache storage; unqualified private blocks a shared cache but can allow a private cache to store. stale-while-revalidate and stale-if-error do not grant either permission.
  2. Freshness. What lifetime applies at this cache? A shared cache uses s-maxage before max-age and Expires; a private cache ignores s-maxage and uses its private-cache lifetime. A response is stale when its current age reaches the lifetime.
  3. Stale permission. A supporting cache may use stale-while-revalidate while it attempts validation, or stale-if-error when the defined error path occurs, within the additional bound. These are permissions, not commands.
  4. Validation. Without permitted stale reuse, a stale entry is conditionally validated or replaced. If-None-Match takes precedence over If-Modified-Since; a successful 304 Not Modified lets 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.

Four gates before a stale response is reused A stored response first passes storage permission, then freshness, then stale permission. If stale reuse is not allowed, the cache validates or replaces the response. Evidence identifies only the named hop. 1 · Store? no-store / privateaudience and key 2 · Fresh? max-age orshared s-maxage 3 · Stale allowed? SWR / SIE,no prohibition Allowed stale result old bytes + background validationor old bytes on an allowed error 4 · Validate or replace conditional 304 or full 200then record named-hop evidence
Read left to right. A stale extension cannot repair storage permission, a cache-key mismatch, a browser/service-worker copy, or a provider policy. The evidence at the end remains scoped to the named hop.

Fresh, stale, and validation states

State at the named cacheConditionExpected actionNot established by this state
Freshcurrent_age < freshness_lifetime, with a matching key and Vary inputsReuse 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 revalidateStale, an applicable stale-while-revalidate window, and no stronger prohibitionReturn 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 errorStale, an applicable stale-if-error allowance, a defined error, and no stronger prohibitionReturn 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 rejectionNo stale permission, an expired/unknown extension, or a prohibitionValidate 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 ageBrowser/private cache: max-age=60Shared cache: s-maxage=300Permissive named implementation onlyWhat remains unknown
0–59Fresh; may reuse without validation.Fresh; may reuse without validation.Same fresh result.Residency, storage at another layer, and what a client displayed.
60Stale. 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.
180Candidate SWR window ends at the boundary; ordinary validation is next unless another permission applies.Fresh.Fresh.A provider may cap or decline the extension.
299Stale and outside the candidate SWR window; validate.Fresh.Fresh.Private and shared caches can be in different states for the same bytes.
300Stale. 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 + 304A 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 + 503Candidate 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 validatorStorage permissionFreshnessStale effectConditional validation
max-age=NCan 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=NShared-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=NNo 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=NNo 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-storeOrdinary 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.
privateUnqualified 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-revalidateDoes 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-revalidateDoes 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.
publicExplicitly 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-MatchValidator 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-SinceValidator 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 ModifiedSuccessful 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

  1. Define “old”: exact URL after redirects, method, query, Vary inputs, 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.
  2. Capture Date, Age, Cache-Control, validators, Vary, Via, Cache-Status, provider fields, request conditionals, and the exact body marker.
  3. Interpret the named Cache-Status member: hit; ttl=120 is fresh reuse; fwd=stale; fwd-status=304 supports stale-then-validated reuse; fwd-status=200 supports a replacement fetch. None names every hidden hop.
  4. Use Age as 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

  1. 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.
  2. Look for a named member such as Edge; fwd=stale; fwd-status=503. It shows the forwarding/error sequence, not by itself that stale-if-error authorized the final body.
  3. Combine the response directives, named provider documentation/configuration, and final body. A hit; ttl=-N can be consistent with stale fallback without forwarding, but it does not name the authorization reason.
  4. Past the allowance, or with no-cache, must-revalidate, applicable s-maxage, or proxy-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

  1. Establish that the named cache was stale, the stale-while-revalidate window applied, and no stronger prohibition blocked stale reuse.
  2. Capture the conditional request. If both validators are present, If-None-Match wins. A 304 keeps the stored representation; a full 200 should replace it under the cache rules.
  3. fwd=stale; fwd-status=304 supports old bytes while unchanged validation runs; fwd-status=200 supports old bytes while a replacement was fetched. collapsed can show a shared forward, but its absence proves nothing about an unreported layer.
  4. 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

SignalWhat a positive value can support at the named hopWhat it cannot prove; what absence means
AgeRFC 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-StatusEach 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.
ViaA 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 fieldsA 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 ModifiedThe 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 markerA 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-store when intentional HTTP-cache retention is unacceptable. Review service-worker, framework, application, history, and provider storage separately; no-store is not a deletion command or complete confidentiality guarantee.
  • If browser retention with validation is acceptable, use a deliberate private policy such as private, no-cache with a validator. Do not add stale extensions unless the private-cache risk is explicitly acceptable.
  • Cookie, Authorization, or Set-Cookie alone 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-age and s-maxage separate. Do not advertise a shared stale period from s-maxage + stale-* without named-cache documentation or an authorized test.
  • If each reuse needs origin confirmation, use response no-cache with validators. Do not assume SWR creates an exception.

Versioning versus purge

NeedPreferBoundary
Routine JS/CSS/image/font releaseNew 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 URLShort 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 objectAuthorized 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 exposureCorrect 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"
CaseFixtureRequired timing/errorExpected outcome if the named cache supports itCompareClaim ledger
Fresh baselinepublic, max-age=60, s-maxage=300; ETag: "a"; body rev-aGET, 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 windowpublic, 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 conflictpublic, max-age=5, s-maxage=10, stale-while-revalidate=20, stale-if-error=30Test 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 SWRpublic, 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 entryFirst 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 outagepublic, 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 windowpublic, 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 precedenceStored 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 chainAny 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

ClaimLabelEvidence requiredStatus here
RFC 5861 defines bounded stale-while-revalidate and stale-if-error response extensions.NormativeRFC 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.NormativeRFC 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.ExpectedNamed-cache documentation plus an authorized timing/error capture.Not tested.
A particular browser HTTP cache honors response stale extensions for a navigation.UnknownBrowser/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.UnknownNamed 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 supportedHeader 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 interpretationRaw 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 correlatedUnique request ID/counter, origin log, and preserved response correlation.No controlled origin or marker capture.
A 304 means the application state was current.Not supportedApplication-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 supportedComplete 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.

SourceUse in this follow-upBoundary
RFC 9111 · HTTP CachingStorage, 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 SemanticsValidators, conditional precedence, 304, and Via.Conditional equivalence at the evaluating recipient is not application freshness or origin attribution.
RFC 9211 · Cache-StatusOptional 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 cachingDeveloper-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 requestsBrowser-facing ETag/Last-Modified and 304 explanation.Does not prove a particular browser or intermediary generated a given 304.
Chrome DevTools · Network and Network referenceHeader, 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 runbookBroader deployment, key, downstream, marker, and purge boundaries.Link used for continuity; this report does not repeat the runbook.
Existing CloudFront report and existing Cloudflare reportNamed-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 reportPrior 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.

Search published pools, pages, reports, and evidence.