# 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
- **Audience:** both, developer
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/docs/technology/http-caching/

## Freshness: the directives that actually decide

| Directive | Effect |
|---|---|
| `max-age=N` | The response may be reused without asking for N seconds |
| `no-cache` | **Store it, but revalidate before every reuse.** It does not mean 'do not cache' |
| `no-store` | Do not write it to disk or memory at all. This is the one that means what people think `no-cache` means |
| `must-revalidate` | Once stale, a cache may not serve it while it re-checks. Removes the 'serve stale on error' allowance |
| `private` | Only a browser cache may store it, never a shared or CDN cache |
| `immutable` | Promise 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.

```http
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.

> **Note** A 304 is not a cache miss. It is the cache working: the body was not transferred. Counting 304s as failures in a dashboard makes a healthy system look broken.

## 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.

> **Careful** This policy trades bandwidth for correctness on purpose. A fingerprinted-filename scheme with `immutable` would be cheaper per request; it also requires a build step that rewrites every import, which this front end deliberately does not have.

## 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.

## Related

- [HTTP](https://connectbyjbrh.com/docs/technology/http/)
- [IndexNow](https://connectbyjbrh.com/docs/technology/indexnow/)
- [Page experience and Core Web Vitals](https://connectbyjbrh.com/docs/technology/core-web-vitals/)
- [Content Security Policy](https://connectbyjbrh.com/docs/technology/csp/)
- [Treating documentation as a build artefact](https://connectbyjbrh.com/research/documentation-as-a-build/)

## What this page is based on

- RFC 9111 — HTTP Caching, https://www.rfc-editor.org/rfc/rfc9111
- RFC 9110 §13 — conditional requests and validators, https://www.rfc-editor.org/rfc/rfc9110
- Connect architecture source pack — docs-source/sources/GENERAL.md §4, the front end is plain ES modules with no build step
- Connect capability registry (docs-source/facts.py) — public_docs
