Skip to main content

Command Palette

Search for a command to run...

HTTP 304, Browser Caching, and Why JavaScript Never Sees It

Published
4 min read
C

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:

  1. Generate correct ETags

  2. Compare If-None-Match

  3. Short-circuit expensive logic when possible

  4. 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.