HTTP caching: Cache-Control and ETag each own half
Cache-Control decides whether a request happens; ETag decides whether the response body can be skipped. Mixing them is why cache bugs persist: changing one never fixes the other half.
Almost every caching problem splits into two independent questions: should this request be sent at all, and once sent, can the body be skipped. Cache-Control owns the first, ETag and Last-Modified own the second.
Half one: whether to send the request
Cache-Control: public, max-age=31536000, immutable
That is what hashed build output should carry: a year, immutable, no request at all.
Cache-Control: no-cache
no-cache is the most misread directive. It does not mean do not cache. It means cache is allowed, but every use must be revalidated. What forbids storing is no-store.
| Directive | Stored | Request sent |
|---|---|---|
max-age=0 |
yes | yes, revalidating |
no-cache |
yes | yes, revalidating |
no-store |
no | yes, nothing written |
immutable |
yes | no, while fresh |
Half two: whether the body can be skipped
The server sends ETag: "abc123"; next time the browser includes:
If-None-Match: "abc123"
Unchanged means 304 Not Modified with no body. You saved transfer, not the round trip. The request still happened.
etag on;
Putting them together
| Goal | Cache-Control | Validator |
|---|---|---|
| Hashed static asset | max-age=31536000, immutable |
none needed |
| HTML page | no-cache |
ETag |
| Cacheable API response | max-age=60, must-revalidate |
ETag |
| Sensitive data | no-store |
pointless |
HTML usually wants no-cache plus ETag: changes go live immediately, and an unchanged page costs one 304 instead of a full document.
Two traps
Changing how ETags are generated invalidates every cache once. If the ETag derives from file mtime, a redeploy re-downloads everything even when nothing changed. A content hash is steadier.
A CDN will rewrite or swallow your headers. The origin says max-age=60 and the CDN may hold it far longer by its own rules. When something will not update, read the CDN status header (eo-cache-status, cf-cache-status) before staring at the origin.
Cache-Control rules whether to ask; ETag rules whether to answer with content. Discuss them together and the bug never closes.

Comments
…