Asking whether anything changed
When a stored response is stale, the browser does not download it again — it asks whether it is still current, carrying what it already has. If the answer is 304, the cached body is reused and the whole exchange is one small round trip.
This is what makes Cache-Control: no-cache cheap: always ask, usually get a 304.
GET /index.html
If-None-Match: "v3"
HTTP/1.1 304 Not Modified
ETag: "v3"
Cache-Control: no-cache
# …and no body at all
Two validators
An ETag is usually a hash of the body or a version number. It compares exactly, it survives a file being rebuilt with the same content, and it works for generated pages where "modified" has no meaning. Send both if you can; the browser prefers the ETag.
| Response header | Request header | Notes |
|---|---|---|
ETag: "v3" | If-None-Match | an opaque id for this exact content — the strong one |
Last-Modified: … | If-Modified-Since | one-second resolution, and clocks disagree |
W/"v3" says "semantically the same, maybe not byte-identical" — right for a response that is gzipped differently each time. A strong ETag is required for range requests, which is why a video server needs one.
The same trick, in reverse: lost updates
Two people open the same record, both save, and the second write silently erases the first. If-Match turns that into an error the client can handle — optimistic concurrency, for the cost of one header. It is the HTTP version of a database transaction's version check.
GET /api/orders/482 → 200, ETag: "7"
PUT /api/orders/482
If-Match: "7" # only if it is still the version I read
→ 200 OK # nobody else changed it
→ 412 Precondition Failed # somebody did: re-read, merge, try again
What a 304 must contain
A 304 has no body and repeats only the headers that could have changed — ETag, Cache-Control, Expires, Vary. The browser merges them into what it already stored. A server that returns a 304 with a fresh Content-Type and no ETag will have the entry revalidated again next time, because there is nothing left to compare.
Where it goes wrong
- A new ETag on every response — a timestamp, or a hash including the current time. Every revalidation becomes a full download.
- Different ETags per server in a cluster, because they hash the file path plus the inode. The same file then looks changed on every other request.
- Returning 200 instead of 304 when the validator matches: correct, and throws away the entire benefit.
- No validator at all on a
no-cacheresponse: every visit re-downloads the whole body.