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.

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. A successful revalidation should return 304 only when the server can prove that the selected representation has not changed.

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”.

Sources