Open your browser’s DevTools, go to the Network tab, and reload any page a second time. Mixed in with the usual 200s, you’ll spot a handful of requests marked 304. Clients ask about this fairly often — is that an error? It’s not. It’s one of the more useful bits of HTTP plumbing, and most site owners never think about it until they’re trying to figure out why repeat visits still feel sluggish.
What actually happens behind a 304
The first time a browser fetches a file — a stylesheet, a script, an image — the server sends the whole thing back with a 200 OK. The browser doesn’t throw that copy away once it’s used. It stores it and remembers a couple of details: when the file was last changed, and a kind of fingerprint of its contents.
On the next visit, the browser doesn’t ask for the file again from scratch. It asks something narrower: “I already have a copy with this fingerprint — is it still good?” If the server confirms nothing changed, it replies with 304 Not Modified and sends no file content at all, just headers. The browser pulls the file straight from its own cache.
ETag, Last-Modified, Cache-Control — three headers that get mixed up
Worth untangling, because these three do related but distinct jobs.
- Cache-Control tells the browser how long to skip asking the server entirely. Something like
max-age=86400means “use the cached copy for a full day, no questions asked.” Until that window closes, there’s no 304 request at all — the file loads instantly, straight from disk, no server round trip. - Last-Modified is the file’s last-changed timestamp. The browser sends it back in an If-Modified-Since header once max-age has run out and it’s time to check.
- ETag is a content fingerprint, more precise than a date. Two files can carry the same modification date after a CI/CD deploy but hold different content — ETag catches that; a timestamp sometimes doesn’t.
Together, these headers are what the “is this still current” check runs on, and 304 is the answer when it comes back yes.

Why this actually matters for speed
Picture an online store’s homepage: stylesheets, a handful of scripts, icons, a logo — a couple of megabytes without much effort. The first visit is always the heaviest one, everything loads cold. The second, third, tenth visit is a different story entirely, assuming caching is set up right.
A shopper comes back an hour later to check another product. The browser already has the styles and scripts cached, so instead of pulling those same two megabytes again, it sends a few short requests that come back 304 — a few hundred bytes each. On a shaky mobile connection somewhere on the go, the difference is physical: a page that should take five seconds opens almost instantly.
There’s a second, less obvious payoff. Google factors repeat-load speed into Core Web Vitals scoring for mobile traffic, and a site that caches its static assets poorly loses ground there even when the first render itself is fast.
How to check whether it’s actually working
Open DevTools (F12), go to Network, make sure “Disable cache” is unchecked — it’s often checked by default whenever DevTools is open, which quietly kills caching while you’re testing — and reload the page twice.
The first load will show mostly 200s. The second, if caching is configured correctly, should show 304 next to stylesheets, scripts, fonts, icons. The Size column often spells out “(from disk cache)” or “(memory cache)” for files that never even reached the server, plus separate 304 entries for the ones that did check in and got confirmed.
If the second load still shows plain 200s at full size across the board, caching isn’t working, and it’s worth digging into why.
What usually breaks it in practice
There are usually a few culprits, and they rarely trace back to the same root cause.
The server just isn’t sending caching headers at all. Common on cheap hosting, or wherever Apache or Nginx is running on defaults without an expires module or explicit rules for static assets. Files ship as 200 every time, even ones that haven’t changed in years.
A CDN and a caching plugin stepping on each other. One layer sets its own Cache-Control headers, the other overwrites them with its own — and the browser ends up with contradictory instructions. We’ve seen a CDN force a short max-age on top of a much longer server-side value, effectively cancelling caching out.
Cache-busting applied too aggressively. The idea itself is sound — appending a version to a filename (style.css?v=123) so the browser is guaranteed to pick up the new version after an update. But if that version string is tied to build time or a random number on every page request instead of the file’s actual content, the browser sees a “new” file every single time, and the exact tool meant to speed things up ends up breaking caching instead.
WordPress optimization plugins can cause the same problem on their own — if a CSS or JS minifier plugin generates a new filename on every page-cache rebuild, the effect is identical: the browser’s old cached copy never gets reused.
How this differs from full-page caching plugins like WP Rocket or W3 Total Cache
This is where confusion usually creeps in, since both get called “caching” even though they operate at different layers.
A full-page caching plugin stores a ready-made HTML page on the server and serves it to the next visitor without hitting the database and PHP again — that speeds up generating the page itself. 304 works somewhere else entirely: it tells the browser not to re-download static files it already has sitting on disk. One saves server time, the other saves the visitor’s time and data.
The best results come from having both running together. The caching plugin speeds up delivering the HTML page, and proper cache headers speed up everything that page then pulls in. Without the second piece, the browser is still stuck re-downloading a couple of megabytes of styles and scripts on every visit.

Where to start
If a DevTools check turns up nothing but 200s where 304s should be, start with the Cache-Control headers on the server for static assets — CSS, JS, images, fonts — then check whether a CDN or optimization plugin is quietly overwriting them. This isn’t something to guess your way through: one wrong header can either kill caching outright or, just as bad, leave the browser showing an outdated file after a site update, which is a much more annoying problem to track down later.