Connect by JBRH Open Connect

HTTP caching and revalidation

HTTP caching has two halves. Freshness decides whether a stored response may be used without asking; Cache-Control sets it. Validation decides what happens when it may not; ETag and Last-Modified let a client ask "has this changed?" and receive a 27-byte 304 Not Modified instead of the whole body.

Status
Reference What this means
Audience
both, developer
Last verified
Product version
6.3.2

Freshness: the directives that actually decide#

DirectiveEffect
max-age=NThe response may be reused without asking for N seconds
no-cacheStore it, but revalidate before every reuse. It does not mean 'do not cache'
no-storeDo not write it to disk or memory at all. This is the one that means what people think no-cache means
must-revalidateOnce stale, a cache may not serve it while it re-checks. Removes the 'serve stale on error' allowance
privateOnly a browser cache may store it, never a shared or CDN cache
immutablePromise that the body at this URL will never change. Only safe with a content hash in the URL

The no-cache misunderstanding causes more production incidents than the rest of the table combined. A response marked no-cache is cached; the cache simply has to check before serving it. That check is cheap when a validator is present, which is why the two mechanisms are designed to be used together rather than chosen between.

Validators: ETag and Last-Modified#

A validator is a token that identifies a specific version of a resource. The client sends it back and the server answers whether it is still current.

GET /assets/app.js HTTP/1.1
If-None-Match: "9f2a1c"
If-Modified-Since: Wed, 10 Sep 2026 08:14:00 GMT

HTTP/1.1 304 Not Modified
ETag: "9f2a1c"
Cache-Control: no-cache, must-revalidate
ETag
An opaque token. Strong ("abc") means byte-identical; weak (W/"abc") means semantically equivalent. Derive it from content, not from a build number, or every rebuild invalidates every file.
Last-Modified
A timestamp with one-second resolution, which is too coarse for anything that changes rapidly. Also easy to falsify by accident: a redeploy that rewrites files gives every file today's date.
If-None-Match
The conditional request header carrying the ETag. Takes precedence over If-Modified-Since when both are sent.
Vary
Names the request headers that change the response. Omitting Vary: Accept-Encoding on a compressed response is how a cache serves gzip to a client that did not ask for it.

Does Connect use HTTP caching, and with what headers?#

Used, with a deliberately conservative policy on application assets. Static assets are served with no-cache, must-revalidate. Read against the table above, that says: keep the file, but ask before every use, and do not serve it from the cache while asking.

The reason is a specific failure this policy prevents. The application front end is plain ES modules with no build step — one module imports another by path, and the imports resolve at run time in the browser. If a browser were allowed to reuse a stale module after a deploy, it would assemble a page out of two different releases: a new module importing a symbol an old one no longer exports, or an old module calling an endpoint that has changed shape. The result is a JavaScript error on a screen that works perfectly on a colleague's machine, and it survives a refresh, which is what makes it so expensive to diagnose.

must-revalidate is the half that matters most. Without it, a cache is permitted to serve a stale response while it revalidates in the background — exactly the mixed-release window the policy exists to close.

Where caching goes wrong#

  • A long max-age on an HTML page. The page is the thing that names every other resource. Cache it hard and you cannot correct anything for the length of the age.
  • immutable without a content hash in the URL. The promise is then false, and the only remedy is a URL change you have not planned for.
  • Caching an authenticated response in a shared cache. Without private or no-store, one person's page can be served to another. This is a data-leak class of bug, not a performance one.
  • **Vary: *.** Legal, and it makes the response uncacheable everywhere. Usually somebody's attempt to say 'be careful'.
  • Cache-busting query strings on assets. Some intermediaries ignore the query when keying, so the bust does not always bust.

Questions#

What is the difference between no-cache and no-store?

no-cache allows storage and requires revalidation before each reuse; the body is usually not re-transferred, because the server answers 304. no-store forbids storage entirely, so every request is a full download. Use no-store for responses that must not exist on disk; use no-cache when you want the bandwidth saving without the staleness.

Should a documentation page be cached differently from an application asset?

It can be. A published page changes rarely and a short max-age with a validator is reasonable. An application module is part of a set that must be consistent with the rest of the deploy, and consistency is worth more there than the round trip.

Why does a hard refresh fix a problem that a normal refresh does not?

A normal refresh revalidates the page but may reuse sub-resources according to their own freshness. A hard refresh bypasses the cache for everything. When a hard refresh is the fix, the bug is nearly always an asset with a cache policy that outlived a deploy.