HTTP 304 vs 200 — Not Modified vs OK (Caching) Comparison

http 304 vs 200: 304 is sent when a conditional GET hits an unchanged resource, saving bandwidth. Learn ETag, If-None-Match, and how 304 cuts CDN/origin costs.

Quick Cheat Sheet

Aspect 304 Not Modified 200 OK
Has response body? No (just headers) Yes
Triggered by Conditional headers (If-None-Match, If-Modified-Since) Anything else
Bandwidth savings High — saves the body transfer None
What client does Reuses cached body Replaces cached body

What Happens in a 304 Round-Trip

The client has previously cached a resource and stored its ETag (or Last-Modified). On a subsequent request, the client sends:

GET /image.png HTTP/1.1
If-None-Match: "abc123"

The server compares abc123 to the current ETag. If they match (the resource hasn't changed):

HTTP/1.1 304 Not Modified
ETag: "abc123"
Cache-Control: max-age=86400

No body is sent. The client uses its cached copy. This is the entire point of conditional GETs — saving the body transfer when the resource is unchanged.

Why 304 Matters for Performance

For large static assets (JS bundles, images, fonts), the body can be hundreds of KB to MB. A 304 response is typically a few hundred bytes of headers — a 100-1000× bandwidth saving for unchanged resources.

CDNs love 304s: Cloudflare, Fastly, and CloudFront all use them aggressively to revalidate stale-but-still-fresh edge cache entries against the origin without re-downloading.

When 200 Is Returned Instead

The server returns 200 (with the body) when:

  • The client didn't send any conditional headers
  • The ETag/Last-Modified doesn't match (resource changed)
  • The server doesn't support conditional requests for this resource
  • A force-refresh request (Ctrl+F5) often strips conditional headers

ETag vs Last-Modified

Two ways to enable 304s:

  • ETag (ETag: "abc123", client sends If-None-Match): opaque hash of the content. Most accurate. Use a strong ETag for byte-identical comparisons or weak (W/"abc") for semantic equivalence.
  • Last-Modified (Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT, client sends If-Modified-Since): timestamp-based. Less precise (1-second granularity, clock skew issues).

ETags are preferred in modern setups but Last-Modified is fine for static files where mtime is reliable.

Common Pitfalls

  • Returning 304 with a body: violates RFC. Some clients ignore the body, others get confused. Always send 304 with no body.
  • Forgetting to include Cache-Control on the 304: the cached copy uses the original Cache-Control from the 200, but you can extend freshness on revalidation by including a new Cache-Control header on the 304.
  • Generating different ETags on different replicas: if two backends produce different ETags for the same content (e.g., due to filesystem-level differences), users will see "always 200, never 304" and bandwidth waste.

Stack-Specific Notes

  • Express: automatically computes weak ETags via the etag setting; sends 304 on If-None-Match matches.
  • Nginx: sends 304 for static files automatically based on Last-Modified.
  • Next.js Route Handlers: must call headers().get('if-none-match') and return new Response(null, { status: 304 }) manually.

Real-World Use Case

A site serving images via a CDN should let the CDN+browser combo revalidate with If-None-Match, returning 304 for unchanged images and dropping origin egress to nearly zero. For a JSON API endpoint that's expensive to compute, hash the response body to generate an ETag and return 304 when clients send a matching If-None-Match — saves both bandwidth and recomputation downstream.

Look Up Any Status Code

Browse all status codes →