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.

← Back to all posts

Comments

…