TL;DR: Use 200 when the server needs to send the resource body. Use 304 when the client already has a cached copy and only needs confirmation that it is still current.
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.
FAQ
Does a 304 response have a body?
No. A 304 response must not include a message body. The entire point is that the client already has the body in its cache. The server only sends headers — typically Cache-Control, ETag, and Vary — to update the cached entry metadata.
What triggers a 304 response?
A conditional request triggers a 304. The client sends If-None-Match (with a cached ETag value) or If-Modified-Since (with a cached date). The server compares these against the current resource. If nothing has changed, it returns 304. If the resource has changed, it returns 200 with the new content.
What is the difference between ETag and Last-Modified for cache validation?
ETag is a content fingerprint (usually a hash) — it changes whenever the content changes, regardless of time. Last-Modified is a timestamp. ETags are more reliable: a file can be modified and restored to its original content (same ETag, different timestamp) or have its timestamp updated without content changes (same content, different timestamp). Prefer ETags when possible.
Can I get a 304 for a POST request?
No. A 304 is only returned for GET and HEAD requests that carry If-None-Match or If-Modified-Since. For POST, PUT, PATCH and DELETE, a failed precondition (If-Match, If-Unmodified-Since) produces 412 Precondition Failed instead (RFC 9110 section 13.1).
Why does my browser sometimes get 200 even when the resource has not changed?
Several reasons: the server may not implement ETags or Last-Modified headers; the Cache-Control header may have no-cache (which forces revalidation but the server still returns 200 if it does not support conditional requests); or the cache entry may have expired and the browser is making a fresh request rather than a conditional one. Check that your server sends ETag or Last-Modified on the initial 200 response.