The Core Difference
200 OK is the standard response: the server sends the full resource — headers and body.
304 Not Modified is a cache validation response: the server confirms the resource has not changed since the client last fetched it. No body is sent. The client uses its cached copy.
304 is not an error — it is an optimization. It saves bandwidth and reduces latency by avoiding retransmission of unchanged content.
The Conditional Request Flow
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
Not sending ETag or Last-Modified — without these headers, the browser cannot send a conditional request and will always get a full 200. Add ETags to your static asset responses.
Confusing 304 with 204 — 304 means “not modified, use cache”. 204 No Content means “request succeeded, no body to return” (used for DELETE or empty API responses). They are unrelated.
Expecting 304 for POST — conditional requests only apply to GET and HEAD. API endpoints that use POST will never return 304.