HTTP 304 Not Modified is a response to a conditional GET or HEAD request. It tells the client that its stored representation is still valid, so the server does not send the resource body again.

The status saves bandwidth, but it is easy to misread. A 304 is not a redirect to another URL, not a blank copy of the page and not proof that every cache has the same object. It only makes sense alongside the client’s cached response and the validator used for the request.

Protocol documentation reviewed November 1, 2026. The exchanges below are illustrative.

The request starts with a cached response

Suppose a browser previously fetched a stylesheet:

HTTP/1.1 200 OK
Content-Type: text/css
ETag: "a1b2"
Cache-Control: max-age=0
Content-Length: 8420

The browser stores the body and response metadata. When it needs the stylesheet again, it can send a conditional request using the entity tag:

GET /site.css HTTP/1.1
Host: example.com
If-None-Match: "a1b2"

If the current representation still matches that tag, the origin can reply:

HTTP/1.1 304 Not Modified
ETag: "a1b2"
Cache-Control: max-age=0
Date: Sun, 01 Nov 2026 04:00:00 GMT

The browser reuses the stored 8,420-byte body. The 304 response itself has no content body.

RFC 9110 defines this as the result of a conditional GET or HEAD that otherwise would have produced a 200 response. The validator changed the transfer decision, not the identity of the resource.

ETag and Last-Modified are validators

An entity tag, or ETag, is an identifier selected by the server for a representation. The client sends it back in If-None-Match. If none of the supplied tags matches the selected representation, the server normally sends the current body. If one matches on a GET or HEAD, a 304 can be returned.

A date validator uses Last-Modified in the stored response and If-Modified-Since in the request. Dates have less precision than entity tags and can be affected by how an application records modification time. When both kinds of condition are present, HTTP gives If-None-Match precedence.

A weak entity tag begins with W/. It can validate that two representations are equivalent enough for cache reuse even when they are not byte-for-byte identical. That differs from range requests, where a client may need a strong validator before joining pieces of a file. Our HTTP 206 explainer covers that case.

A 304 response does not carry the missing page

Opening a 304 row in developer tools can show a response with zero transferred body bytes. That does not mean the browser displayed nothing. The visible document, script, image or stylesheet came from the stored response.

The client combines stored metadata with fields sent in the 304. RFC 9110 requires a server generating 304 to include fields such as Date, ETag, Vary, Content-Location, Cache-Control and Expires when those fields would have been sent in the corresponding 200 response.

That update can change how long the stored response remains fresh. The cache is not simply replaying an old object forever; it is revalidating and updating metadata according to the response.

Freshness and validation are different

A fresh cached response can be reused without contacting the server. A stale response may need validation before reuse. The request and response therefore follow one of three broad paths:

Cache state Network action Typical result
Fresh No validation request required Stored response reused
Stale, validator matches Conditional request 304, stored body reused
Stale, validator does not match Conditional request 200 with current body

Cache-Control: max-age=3600 can make a response fresh for an hour after its stored time. max-age=0 does not forbid storage; it normally means the response becomes stale immediately and must be validated before reuse. no-store is the directive intended to prevent a cache from storing the response.

This distinction is useful when debugging. Seeing a network request on every reload does not necessarily mean caching is disabled. The browser may be validating a stored object and receiving a small 304 each time.

What Vary changes

The Vary response header tells a cache which request fields affect representation selection. If a response varies on Accept-Encoding, for example, a compressed representation and an uncompressed representation need separate cache keys.

A validator should be evaluated for the representation selected by the current request. Reusing an ETag from a different variant can cause confusing results. When a CDN or reverse proxy participates, also check which layer generated the tag and which request headers are part of the cache key.

Our CDN explainer describes how an edge cache can answer before a request reaches the origin. A browser may validate with the edge; the edge may separately validate with the origin. Those are related but distinct cache decisions.

Why you sometimes see 200 instead

A conditional request can still receive 200 OK. Common reasons include:

  • the resource changed and the ETag no longer matches;
  • the server does not support or honor the validator;
  • the request selected a different representation because of Vary;
  • an intermediary removed or changed the conditional header;
  • developer tools or a hard reload instructed the browser to bypass normal reuse.

A 200 is not automatically a cache failure. It is the correct response when the server needs to transfer the current representation.

Conversely, a 304 without a usable stored response is not enough to reconstruct the resource. HTTP expects the recipient to have a matching stored representation. If a client discarded the body but retained only a validator, it should not treat the 304 as content.

304 is different from redirects and partial content

HTTP places 304 in the 3xx class, but it is not a location redirect. There is no required Location header and the browser does not follow a new URL. The instruction is to reuse a stored representation.

It also differs from 206 Partial Content. A 206 response has a body containing the requested range. A 304 response has no body. Both can reduce transferred bytes, but they solve different problems: 206 selects part of a representation; 304 confirms that the client already has the current representation.

And it differs from HTTP 429. A 429 asks a client to slow down. A 304 confirms cache validity. Retrying a valid 304 more aggressively only creates unnecessary traffic.

A practical debugging checklist

In the browser network panel, first disable any “Disable cache” option. Reload once normally and inspect the request method, status and transferred-size column. Open the request headers and look for If-None-Match or If-Modified-Since.

Then compare the response’s ETag with the previous stored value. Check Cache-Control, Expires, Age and Vary. If a CDN is involved, note response headers that identify the answering layer, but do not assume those headers are standardized across providers.

For a command-line check, save an ETag from a 200 response and send it in a later request:

curl -i https://example.com/site.css
curl -i -H 'If-None-Match: "a1b2"' https://example.com/site.css

Use the actual ETag returned by the server. The quoted value in this example is invented.

Finally, test the behavior you care about. A 304 can save bandwidth while still requiring a round trip. If latency matters more than transfer size, review freshness directives so the browser can reuse a fresh response without validation where that is safe.