The Fast Mental Model
If you are looking at DevTools and wondering why the browser got “no real response body,” the answer is usually this:
- 200 OK means “here is the representation”
- 304 Not Modified means “use the copy you already have”
304 Not Modified is not a failure and it is not an empty 200. It is a successful cache revalidation response.
The Core Difference
200 OK is the baseline response. The server sends headers and a full body.
304 Not Modified only appears when the client asks a conditional caching question, such as “has this changed since the version with this ETag?” If the answer is no, the server skips the body and the client reuses its cached content.
Why You See 304 In Real Life
You usually encounter 304 in one of these situations:
- refreshing a page where CSS or JS is already cached
- debugging stale assets and watching
ETagorLast-Modified - trying to understand why the browser made a network request but did not download the full file again
304 only happens when the client sends a conditional request:
# First request — client has nothing cached
GET /styles.css HTTP/1.1
# Server responds with full content + cache validators
HTTP/1.1 200 OK
ETag: "abc123"
Cache-Control: max-age=3600
[full CSS body]
# Later — cache has expired, client revalidates
GET /styles.css HTTP/1.1
If-None-Match: "abc123"
# Resource unchanged — server sends 304, no body
HTTP/1.1 304 Not Modified
ETag: "abc123"
Cache-Control: max-age=3600
Comparison Table
| 200 OK | 304 Not Modified | |
|---|---|---|
| Response body | Yes (full resource) | No |
| Triggered by | Any GET/HEAD | Conditional GET (If-None-Match / If-Modified-Since) |
| Client action | Store in cache | Use existing cached copy |
| Bandwidth used | Full resource size | Headers only (~200–500 bytes) |
| Requires prior request | No | Yes (needs cached ETag or Last-Modified) |
ETag vs Last-Modified
Servers can use two mechanisms to enable conditional requests:
ETag — a content fingerprint, usually a hash of the response body:
ETag: "d41d8cd98f00b204e9800998ecf8427e"
Client sends back as If-None-Match: "d41d8cd98f00b204e9800998ecf8427e".
Last-Modified — a timestamp of when the resource last changed:
Last-Modified: Tue, 18 Feb 2026 00:00:00 GMT
Client sends back as If-Modified-Since: Tue, 18 Feb 2026 00:00:00 GMT.
ETags are more reliable. A file can be regenerated with identical content (same ETag, different timestamp) or have its mtime updated without content changes. Use ETags when your server can compute them cheaply.
Cache-Control Interaction
304 only comes into play after a cache entry expires. The flow is:
- Fresh cache (
max-agenot exceeded) — browser uses cached copy directly, no request sent - Stale cache (
max-ageexceeded) — browser sends conditional request → server returns 304 or 200 - No cache validators (no ETag, no Last-Modified) — browser must fetch full 200
Cache-Control: no-cache forces revalidation on every request (step 2 always), but still allows 304 if the server supports it. Cache-Control: no-store disables caching entirely — always 200.
Common Mistakes
Treating 304 like a missing response — it is not missing anything. The response is telling the client that the cached body is still authoritative.
Not sending ETag or Last-Modified — without validators, the browser cannot revalidate efficiently and must fall back to full 200 responses.
Confusing 304 with 204 — 304 means “reuse your cached copy.” 204 No Content means “the request succeeded, and there is no body to send for this operation.”
Expecting 304 for POST — conditional cache revalidation is for GET and HEAD, not for write-oriented methods.