Caching is not a single switch. A response has a freshness decision made by a cache and a validation conversation with the origin. Mixing those two ideas produces either stale content or needless requests.
Pick the change model first
For an HTML document that may change after publication, Cache-Control: no-cache is usually a more accurate starting point than no-store. no-cache permits storage but requires a cache to validate before reuse; no-store asks caches not to store the response at all. The distinction matters when you want conditional requests and a cheap 304 Not Modified response.
For a fingerprinted asset such as app.4f2c.js, the URL changes when the bytes change. A long freshness lifetime can then be reasonable:
Cache-Control: public, max-age=31536000, immutable
That policy is unsafe for a stable URL whose contents can be replaced in place. If the URL is stable, prefer a shorter lifetime and a validator such as an entity tag (ETag) or a meaningful Last-Modified value.
Choose who may store the response as well as how long it stays fresh. Use the unqualified private directive when shared caches must not store user-specific content; a browser’s private cache can still store it. no-cache alone does not provide that restriction. The public example above is for assets safe to share across users, not a default for personalized responses.
Inspect both paths
Make one ordinary request and one conditional request. The exact server flags vary, but the evidence you want is stable:
curl -sS -D - -o /dev/null https://www.example.test/
curl -sS -D - -o /dev/null \
-H 'If-None-Match: "copy-a-real-etag-from-the-first-response"' \
https://www.example.test/
Do not paste a made-up validator into a runbook. Copy the value from the first response or use a test fixture. For this conditional GET, a matching If-None-Match can yield 304 using weak comparison: it permits semantic equivalence, not necessarily byte identity. A changed validator normally produces a new 200 response. Keep the ETag’s quotes and any W/ prefix intact.
Keep intermediaries visible
Age, Vary, and the cache-control directives explain why two clients can receive different results. If a response varies by Accept-Encoding, language, or another request field, that dimension belongs in Vary; otherwise a shared cache can reuse the wrong representation.
The useful release check is not “the scanner found a cache header”. It is “the header matches the replacement policy, and a conditional request behaves as documented”.
immutable applies during the advertised freshness lifetime; it is not a permanent ban on revalidation. Keep versioned URLs immutable and older assets reachable while old HTML can still reference them. Changing today’s cache header does not retroactively shorten a previously cached response’s lifetime, so a cache-policy mistake may require a new URL or an explicitly supported cache purge.
Sources
- RFC 9111 — HTTP Caching, sections 4–5, checked 2026-10-15.
- RFC 9110 — HTTP Semantics, sections 8 and 13, checked 2026-10-15.
- MDN:
Cache-Control, directive semantics and defaults, checked 2026-10-15. - RFC 8246 — Immutable Responses, freshness-limited immutable semantics, checked 2026-10-15.