HTTP 304, Browser Caching, and Why JavaScript Never Sees It
I can write c#, golang, php, python, rust, ziglang, javascript, SQL code and blog on these languages.
If you’ve ever implemented ETag-based caching in a REST API, you may have run into a confusing moment:
“My server is clearly returning 304 Not Modified*, but JavaScript still sees **200 OK**. Is the browser broken?”*
Short answer: No. The browser is doing exactly what it’s supposed to do.
This article explains why 304 exists, how browsers handle it, why JavaScript is intentionally unaware of it, and how backend and frontend developers should think about this behavior when designing real-world web applications.
1. What HTTP 304 Really Means
304 Not Modified is not a success response for applications.
It is a cache validation response.
It tells the browser:
“The representation you already have is still valid. Reuse it.”
Key points: - A 304 response never contains a body - It is only valid when the request includes a conditional header like: - If-None-Match (ETag) - If-Modified-Since
304 exists for caches, not for application logic.
2. The Three Layers Involved (This Is the Key Insight)
When a browser makes a request, there are three distinct layers involved:
┌────────────────┐
│ JavaScript │ ← fetch(), axios, etc.
├────────────────┤
│ HTTP Cache │ ← ETag, 304, revalidation
├────────────────┤
│ Network │ ← actual HTTP exchange
└────────────────┘
Most confusion comes from mixing these layers.
304 lives in the cache layer
JavaScript lives above the cache layer
JavaScript does not talk directly to the network.
3. What Actually Happens Step by Step
First request
GET /api/movies
Server response:
200 OK
ETag: "movies:v42"
{ ...json data... }
Browser behavior: - Stores JSON + ETag in cache - JavaScript receives 200 and data
Second request (data unchanged)
Browser sends automatically:
GET /api/movies
If-None-Match: "movies:v42"
Server responds:
304 Not Modified
Now the important part:
Browser reuses cached JSON
Browser merges cached body with headers
Browser returns a synthetic 200 response to JavaScript
JavaScript sees:
res.status === 200
await res.json(); // works
Even though the server returned 304.
This is intentional.
4. Why Browsers Hide 304 From JavaScript
If browsers exposed raw 304 responses to JavaScript:
Every app would need manual cache handling
Developers would reimplement broken caching logic
Performance would suffer
So browsers made a clear design choice:
304 is an internal optimization, not an application-level signal.
JavaScript only sees usable representations.
5. Why 204 “Works” but 304 Seems Invisible
This often confuses developers.
204 No Content
Means: “Success, intentionally no body”
JavaScript must know this
Browser passes it through unchanged
304 Not Modified
Means: “Use what you already have”
JavaScript does not need to know
Browser handles it internally
They may both have no body — but their meaning is completely different.
6. What This Means for Frontend Developers
❌ Do NOT do this
if (res.status === 304) {
// reuse cached data
}
This will never work reliably.
✅ Do this instead
const res = await fetch(url, {
cache: "no-cache",
credentials: "include"
});
const data = await res.json();
render(data);
Let the browser handle caching.
Your JavaScript code should not care whether the data came from: - the network - memory cache - disk cache - a 304 revalidation
7. What This Means for Backend Developers
Backend responsibility is simple and powerful:
Generate correct ETags
Compare If-None-Match
Short-circuit expensive logic when possible
Return:
304 when unchanged
- 200 + ETag when changed
Best practice rule
If a GET response has a body, it should have an ETag.
This applies to: - lists - paginated endpoints - search results - single resource fetches
8. Pagination, Search, and “Smart” Browser Caching
Browsers cache per URL.
That means:
/api/movies?page=1
/api/movies?page=2
are cached independently.
If you use a shared dataset version in your ETag (for example, movies:v42):
All pages invalidate together
No client-side pagination cache logic is needed
This is how REST was meant to work.
9. DELETE, UPDATE, and Cache Invalidation
Any operation that changes what a GET would return must change the ETag.
That means: - CREATE → bump version - UPDATE → bump version - DELETE → bump version
Many systems achieve this using a metadata document:
{
"movies_version": 43,
"movies_last_updated_on": "2025-07-16T12:10:00Z"
}
ETag example:
ETag: "movies:v43"
10. The Right Way to Think About 304
Here is the mindset shift that matters:
304 is not part of your application logic.
It is a transport-level optimization handled by browsers.
Your job as a developer is to: - design stable representations - generate correct validators (ETag) - trust the browser
When you do that, you get: - better performance - less bandwidth usage - simpler frontend code
For free.
Final Takeaway
JavaScript never “sees” 304 — by design
Browsers are extremely good at caching
ETag + 304 is a solved problem
Don’t fight it — use it
If you let HTTP do its job, your application becomes faster, simpler, and more scalable — without extra code.
That’s not magic.
That’s good protocol design.
