Skip to main content

Guides / HTTP status codes explained

304 Not Modified: why it is a success, not an error

304 Not Modified is a success: it means your cached copy of the resource is still current, so the server is sending no body and telling you to reuse what you already have. It only ever appears in reply to a conditional request, which is why a monitoring check that sends no cache headers will never see one.

How a 304 happens

A 304 is the second half of a two-step conversation. On the first request the server returns the resource with a validator: an ETag, which is an opaque identifier for that exact version, and often a Last-Modified date. The client stores the response together with those validators.

Later, when the cached copy needs revalidating, the client repeats the request and includes what it has:

  • If-None-Match: "abc123" carries the stored ETag. This is the preferred validator because it is exact.
  • If-Modified-Since: Wed, 03 Sep 2026 10:00:00 GMT carries the stored date, used when there is no ETag.

If the resource has not changed, the server answers 304 Not Modified with headers only and no body, and the client renders its cached copy. If it has changed, the server answers 200 with the new content and new validators. Both outcomes are correct behavior.

Why a 304 is good news, not an error

People see 304 in a browser network panel or a log analyzer, notice it is not a 200, and assume something failed. Nothing did. A 304 is the cheapest useful response on the web: no response body crosses the network, so the transfer is a few hundred bytes instead of the full asset, and the server usually avoids regenerating the resource, so it costs less CPU than a 200 would. The page renders faster because the browser reuses bytes it already has. Crawlers benefit too, since a stable ETag that produces 304s lets a search engine spend its crawl budget on pages that changed.

A high proportion of 304s on static assets is a sign that caching is configured well. The one thing to watch is whether you need the revalidation round trip at all: for versioned or fingerprinted assets a long Cache-Control: max-age with immutable avoids even the 304, because the browser does not ask.

When a 304 is a problem

  • A stale page that will not refresh. If your HTML is served with a validator that never changes while the content does, browsers keep getting 304 and users keep seeing the old page. This is the classic "I deployed but nobody sees it" bug.
  • A 304 with no matching cached copy. If a client sends a conditional request it did not have grounds for, or a proxy strips the body from a 200, the client is left with nothing to render and the page appears blank.
  • Weak or unstable ETags behind a load balancer. When several backends generate different ETags for identical content, every revalidation misses and you get full 200s where 304s were expected. Traffic goes up for no reason.
  • Compression changing the validator. If the ETag differs between compressed and uncompressed variants without a correct Vary: Accept-Encoding, caches can serve the wrong thing.

How to test conditional requests

  1. Fetch the resource and read its validators:
    curl -sI https://example.com/style.css | grep -Ei "^(HTTP|etag|last-modified|cache-control)"
  2. Repeat the request with the ETag you were given and confirm the server answers 304:
    curl -sI -H 'If-None-Match: "abc123"' https://example.com/style.css | head -n 1
    Substitute the exact ETag value, quotes included. A 200 here means revalidation is not working.
  3. If you get 200 when you expected 304, check whether a proxy or CDN is stripping If-None-Match, and whether your backends agree on the ETag for the same file.
  4. If you get a stale page, make the validator change when the content changes. Fingerprinted filenames are the most reliable fix; otherwise derive the ETag from the content itself.
  5. Keep HTML on a short freshness lifetime and let assets carry the long one. HTML that is cached for a day is what makes a deploy invisible.

Why uptime checks never see a 304

An uptime check requests a page as a first-time visitor, with no If-None-Match and no If-Modified-Since, so the server has nothing to compare against and always answers 200. That is the right default for availability. It also means caching regressions do not show up in uptime data at all: a site can serve everyone a stale page while every check stays green. Assert on page content as well as status, and a page that stopped updating becomes visible as a content mismatch. HostTracker offers 13 monitor types and has been monitoring websites since 2004. Test headers and status with the HTTP check tool, and see what a 200 OK really proves and the full 3xx family guide.

Check it now

Run the free check against your own site - no account needed.

HTTP check

Monitor this permanently

Get alerted the moment it breaks: HostTracker checks from 300+ locations and notifies you by email, SMS, Slack, Telegram and more.

HostTracker features

More in this section: HTTP status codes explained