3xx redirectionNo bodyHeaders: ETag, Last-Modified
304 Not Modified
The copy the client already has is still current, so the server sends headers only. It answers a conditional GET or HEAD: one that carries If-None-Match or If-Modified-Since.
Open Hash Generator An ETag is often a hash of the content.
When you see it
- The browser revalidates a cached file whose
max-ageran out, sending theETagit stored asIf-None-Match. - A CDN or proxy checks its copy against the origin.
- You pressed reload: browsers revalidate the page itself even while it is still fresh.
What to check
- A 304 is a success. If your code treats it as an error, the HTTP client isn't handling conditional requests; browsers and
fetchhand you the cached body with status 200. - Stale content despite 304s means the validator didn't change when the content did. An
ETagbuilt from file size and modification time can survive a deploy; a hash of the content can't. - A proxy that compresses on the fly may rewrite the ETag (Apache adds
-gzip) or drop it. ThenIf-None-Matchnever matches, and every request costs a full 200: compare the ETag the client stores with the one the origin checks. - Files with a content hash in their name (
app.3f9a1c.js) don't need revalidation at all: serve them withCache-Control: max-age=31536000, immutable.
304 or a 200 from cache?
A 304 costs a round trip to the server, just without the body. A fresh cached response (DevTools shows "from disk cache") costs no request at all. max-age decides which of the two you get.
304 or 412?
Both answer a conditional request. If-None-Match on a GET gets a 304 when nothing changed. If-Match on a PUT gets a 412 when something did, which is how APIs stop lost updates.
Example
GET /app.css HTTP/1.1 Host: example.com If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4" HTTP/1.1 304 Not Modified ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4" Cache-Control: max-age=3600
Defined in RFC 9110, section 15.4.5.