HTTP 304 Not Modified: The Response That Saves Bandwidth
A junior developer once pinged me in a mild panic: "The network tab is full of 304 errors on our site." I got to explain one of my favorite facts about HTTP — those weren't errors. Every one of those 304s was the server saying "you already have the current version of this file, I'm not sending it again," and each one saved a full download. The page was fast because of the 304s.
The HTTP 304 status code is the only status in common use whose entire purpose is to send nothing. Understanding the handshake behind it tells you a lot about how well your site caches.
The conversation behind a 304 status code
A 304 never happens on a first visit. It's the second half of a two-part exchange called a conditional request:
- First visit: the browser requests
/styles.css. The server sends 200 with the full file, plus validators — aLast-Modifieddate, anETag(a fingerprint of the content), or both. - Next visit: the browser still has the file cached but wants to confirm it's fresh. It sends the validators back:
If-Modified-Sincewith the stored date, orIf-None-Matchwith the stored ETag. - The server compares. Unchanged? It answers 304 with empty body — a few hundred bytes instead of the full asset. Changed? It sends a fresh 200 with the new content and new validators.
Note that a conditional request is still a request. The 304 saves transfer, not the round trip. That's why long Cache-Control: max-age values matter too — they let the browser skip even the question for a while. The 304 is the safety net for when the browser does ask.
Reproducing a 304 by hand
You can play the browser's role with two curl commands. First, fetch the validators:
curl -sI https://example.com/styles.css | grep -iE "etag|last-modified"
Then send the ETag back as a condition:
curl -sI -H 'If-None-Match: "abc123"' https://example.com/styles.css | head -1
If the setup works, that second command returns HTTP/2 304. If it returns 200 with the full body every time even though nothing changed, your conditional request handling is broken somewhere — and every repeat visitor and every crawler is paying for it.
Why Googlebot loves your 304s
Googlebot uses conditional requests constantly. When it revisits a URL it has seen before, it often asks with If-Modified-Since. A well-behaved server answering 304 for unchanged pages lets Googlebot verify freshness at almost zero cost — which means the crawler gets through more of your site per visit. On large sites this is a real crawl budget lever: thousands of unchanged URLs confirmed via cheap 304s leave room for the crawler to spend its time on new and updated content instead.
A 304 has no direct ranking effect — the page keeps whatever standing it had, since the crawler is confirming its cached copy is still valid. The SEO win is entirely in crawl efficiency and server load.
When 304s go missing
If your server never sends 304s, one of these is usually the culprit:
- No validators sent. Dynamically generated pages often ship without
Last-ModifiedorETagunless the application sets them. No validator, nothing to condition on, no 304 possible. - ETags that change on every request. If the fingerprint includes a timestamp, a session token, or anything volatile, every comparison fails and every response is a full 200. Compression can cause this too — some servers compute the ETag after gzip, and varying compression settings shift the fingerprint.
- Load-balanced servers with default Apache ETags. Older Apache included the file's inode in the ETag, so the same file on two backend servers produced two different fingerprints. Behind a load balancer that meant near-constant validation misses. The fix is
FileETag MTime Size. - A CDN or proxy stripping conditional headers before they reach origin, or answering on origin's behalf without validators of its own.
When a 304 causes real trouble
The failure mode in the other direction is rarer but nastier: a server answering 304 for content that did change. Users get stale pages, and no amount of refreshing fixes it because the server keeps insisting their cache is current. I've traced this to hand-written caching code comparing dates with the wrong timezone, and to a proxy caching one variant of a page and validating everyone against it. If users report seeing old content, make the comparison from the terminal: fetch the live page uncached, then send a conditional request with an old date and see whether the server correctly returns a fresh 200.
Also keep the semantics straight: 304 means "your copy is current," not "this page is fine." A page can validate perfectly and still be a soft 404 or broken template that's simply consistent about it.
Making conditional requests work for you
The checklist I apply on every performance audit: static assets get long max-age plus an ETag or Last-Modified, so revalidation is possible after expiry. HTML gets shorter max-age but always a validator, because HTML is what crawlers revalidate most. Then verify from outside — run a site-wide crawl that records response headers and confirm your key page types actually return validators, because the gap between "configured in nginx" and "surviving the CDN to reach the client" is where most caching setups quietly fail.
Frequently Asked Questions
Is HTTP 304 an error I need to fix?
No. A 304 means the browser asked whether its cached copy was still current and the server confirmed it was, skipping the download. Lots of 304s in your network tab means caching and revalidation are working. The situations to fix are the opposite ones — no 304s at all, or 304s served for content that actually changed.
What headers are required for a 304 to happen?
The server must first send a validator with the full response — a Last-Modified date, an ETag, or both. The client then repeats the request with If-Modified-Since or If-None-Match carrying that value. Without a validator on the original response, the client has nothing to condition on and every request returns a full 200.
Do 304 responses help SEO?
Indirectly but meaningfully on large sites. Googlebot sends conditional requests when revisiting known URLs, and cheap 304 answers for unchanged pages let it cover more of your site per crawl session. That efficiency means new and updated content gets discovered faster; the code itself doesn't change rankings.
Why does my server never return 304?
Usually because responses lack validators (common with dynamic pages), because ETags include something volatile like a timestamp or inode so they never match, or because a CDN strips conditional headers. Test with curl by sending If-None-Match with the ETag you received — if you get 200 instead of 304, trace where the validation breaks.
Try WebsiteChecker.Tech Free
Run a free technical SEO audit on any website. Get a client-ready report in minutes.
Start Free Scan