Developer Toolbox
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-age ran out, sending the ETag it stored as If-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 fetch hand you the cached body with status 200.
  • Stale content despite 304s means the validator didn't change when the content did. An ETag built 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. Then If-None-Match never 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 with Cache-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.