ByteScope

304

How to fix HTTP 304 Not Modified

3xx redirectionIETF standardRFC 9110

Your copy is still good: you sent a validator with the request, the server compared it against the one it holds, they matched, and it deliberately sent headers and no body.

Why you are seeing 304, and what to do about it

The same three digits are three different problems depending on which side of the request you are on. Read the block that describes you.

Reproduce 304 with curl

Run this against the URL that failed. It prints the status without the error page, so you can see what the server said rather than what the browser rendered.

curl
curl -sS --etag-save /tmp/etag -o /dev/null https://example.com/style.css && curl -sS -D - -o /dev/null --etag-compare /tmp/etag https://example.com/style.css

The first request saves whatever `ETag` the response carried into a file; the second sends it back as `If-None-Match`, which is precisely what a browser cache does on a revalidation, so this is a real 304 rather than a simulated one. `-D -` dumps the second response's headers, and the status line is the whole answer: `304` means the tag matched. Read four things off it. Was there an `ETag` in the first response at all — no tag and no `Last-Modified` means nothing on that URL can ever be revalidated, which explains a site that re-downloads itself on every visit. Does the 304 repeat the tag, as RFC 9110 requires? Does a `W/` prefix appear only once compression is negotiated — run the second command with and without `-H 'Accept-Encoding: gzip'`, and if one gets a 304 while the other gets a 200, the validator is per-encoding and `Vary: Accept-Encoding` is what the cache must key on. And which layer answered: `server:` and any `cf-ray` tell you whether the 304 came from an edge holding its own copy or from your origin. For the date-based path use `-z` instead, which curl documents as `--time-cond`: it builds an `If-Modified-Since` from a date you name or from a local file's timestamp, and a leading dash inverts it into `If-Unmodified-Since`.

What 304 actually means

RFC 9110 §15.4.5 defines 304 as the answer to a conditional GET or HEAD that would have returned 200 if the precondition had not evaluated to false. Two rules in that section matter while debugging. A 304 cannot carry content — it is terminated by the first empty line after the header fields — so middleware that writes a body onto one corrupts message framing rather than merely wasting bytes. And a 304 must repeat the header fields a 200 would have carried that the cache needs, which RFC 9110 names as `Content-Location`, `Date`, `ETag` and `Vary`, so a 304 with no `ETag` on it is already suspicious. The condition itself comes from §13: `If-None-Match` compares entity tags using the weak comparison function, `If-Modified-Since` compares an HTTP-date, and §13.2.2 sets the precedence — `If-Modified-Since` is only evaluated when `If-None-Match` is absent, which means a request carrying both is decided entirely by the tag and your `Last-Modified` never enters the argument. The two validators are not equally sharp: an HTTP-date has one-second resolution (§5.6.7), so two edits inside the same second are indistinguishable, whereas an entity tag is whatever the origin says it is and is strong unless it is prefixed with `W/`.

Codes 304 gets mistaken for

Confused withHow to tell them apart
200A 304 in the network panel and a 200 in your JavaScript are frequently the same exchange. The browser's HTTP cache issues the conditional request, receives the 304, and resolves your `fetch()` with the stored 200 — so the status your code observes is 200 even though the bytes never crossed the network. Seeing 304 in application code means the conditional header was yours.
412The same failed precondition produces different codes depending on what you were trying to do. A validator that does not match on a GET is a 304, because you already hold a usable copy; on a state-changing method — `If-Match` on a PUT that lost the race — it is 412 (Precondition Failed, RFC 9110 §15.5.13), because there is nothing to reuse and the write must not proceed.
no-cache`no-cache` is what *creates* 304 traffic: it permits storage but requires revalidation before reuse, so every request asks and most answers are 304. `no-store` forbids keeping the response at all, so there is never a validator to send and never a 304 to receive. Reaching for `no-store` to fix a staleness bug throws away the cheap revalidation along with the stale copy.
ETagStrength is a real distinction and only one field cares. `If-None-Match` uses the weak comparison function, so `W/"v1"` and `"v1"` match and a gzip-weakened tag still yields 304s. `If-Range` and `If-Match` use strong comparison, so the same weakened tag silently disables range requests — which is why a large file re-downloads from zero after a dropped connection on a server that gzips.
Reason phrase
Not Modified
Class
3xx redirection
Defined by
RFC 9110
Standard
IETF standard

Tools on this site

All of these run in your browser — nothing is uploaded.

Related status codes

Codes people usually end up reading in the same session as 304.

Frequently asked questions

How do I find out which server produced this status code?

Read the response headers rather than the page. curl -sS -o /dev/null -D - https://example.com/path prints them without the body, and server, via and cf-ray between them name the layer that answered. A cf-ray value means Cloudflare handled the response and is the id their support will ask for. To take the edge out of the picture entirely, repeat the request with --resolve example.com:443:203.0.113.10, which connects to the origin address you name while still sending the original host name and SNI — if the answer changes, the edge and the origin disagree.

Does the reason phrase after the number matter?

No, and you should never branch on it. RFC 9112 tells clients to ignore the reason phrase, servers are free to change it, and HTTP/2 and HTTP/3 do not carry one at all — so 404 Not Found over HTTP/1.1 arrives as a bare 404 over HTTP/2. The phrases on this site are the registered ones because they are what people search for and what appears in an HTTP/1.1 log, not because any software depends on them.

Should I retry a request that failed with this code?

Retry 5xx and 429; do not retry 4xx, because nothing about the request will be different next time. If the response carries Retry-After, honour it — RFC 9110 defines it for exactly this, and it may be either a number of seconds or a date. Otherwise use exponential backoff with jitter and a hard cap, and only for idempotent methods: a retried POST can charge a card twice. A retry storm against a server that is already failing is how a brief incident becomes a long one.

Can I make my server return a different status code?

Yes, and the only rule that matters is that the code has to be true. Anything that reads your responses automatically — search engines, monitoring, caches, client retry logic — makes decisions from the number and never from the page. Serving an error page with 200 hides the failure from your own alerting; serving 200 for a missing page gets the error indexed as content; returning 500 for a request that was simply malformed sends whoever is on call to the wrong half of the stack.

Still stuck on 304? browse every status code in the reference — or go back up to the block written for your side of the stack.