Back to News & Insights
Web Development September 13, 2026 · 7 min read

Why Your React Hotfix Isn't Reaching Users - And How to Fix It with Nginx

A deep dive into browser caching, heuristic TTL, ETags, and the two Nginx config lines that guarantee every deploy reaches every user instantly.

Why Your React Hotfix Isn't Reaching Users - And How to Fix It with Nginx

You push a critical hotfix. Deploy succeeds. You refresh the page and see the fix. You tell your team it's live. Then a user messages you:

You've hit the browser cache problem. It's one of the most misunderstood parts of frontend deployment, and the fix is two lines of Nginx config. But to truly understand why those two lines matter, you need to understand what the browser is actually doing behind the scenes.

When a user visits your React app, there are two places a response can be cached before it reaches them: The browser cache, stored on the user's own machine Nginx itself, but only if you've explicitly enabled proxycache (more on this below)

For most React apps, Nginx is just a static file server. It reads your dist/ folder from disk and sends files to the browser. There is no in-memory Nginx cache in this setup. Nginx reads the file on every request. The caching happens entirely in the browser.

This distinction matters because a lot of developers assume Nginx is caching on their behalf. It isn't, unless you've explicitly configured proxycache, which is a separate feature used when Nginx sits in front of a backend server like Node or Django.

Every time the browser receives a file from Nginx, it looks at the response headers to decide how long to store it. There are two scenarios.

The browser follows exactly what you tell it. max-age=86400 means cache for one day no-cache means always revalidate before using immutable means never check again

This is where most developers get surprised. The browser doesn't just skip caching. It applies heuristic caching, defined in RFC 7234. Here's the formula it uses:

Nginx automatically sends a Last-Modified header for every static file it serves. It's the file's modification timestamp on disk. The browser uses that to calculate a TTL.

| File last modified | Heuristic TTL | | --- | --- | | 1 day ago | 2.4 hours | | 10 days ago | 1 day | | 60 days ago | 6 days (most browsers cap around 7 days) |

This is the trap. Right after a fresh deploy, Last-Modified is just a few seconds ago, so the TTL is near zero and everything seems fine. But as days pass without a deploy, the TTL silently grows. The very moment you need an urgent hotfix to reach users fast is exactly when the cache is fighting you hardest.

Understanding the exact sequence is important. Here's what happens step by step when a user visits your app.

Browser downloads the file. Nginx sends it with a Last-Modified header and an ETag (a fingerprint of the file content, something like "65a3f-abc123"). The browser stores the file, the ETag, and calculates a heuristic TTL.

Here's the critical part that most people miss. The browser does not immediately contact the server. It first asks: is the TTL still valid? TTL still valid: serve from cache. Zero network request. The server is never contacted. The ETag is never checked. Your deploy never gets picked up. This is the bug. TTL expired: send a conditional request to Nginx with If-None-Match: "abc123"

Nginx receives the request and compares the ETag the browser sent against the current file on disk. File unchanged: Nginx replies 304 Not Modified. No file body is sent, just a tiny header response. The browser uses its cached copy and recalculates the TTL (which is now larger, since the file is older). File changed: Nginx replies 200 OK with the fresh file and a new ETag. The browser downloads it and recalculates TTL (which is now small, since the file was just modified).

The key insight: the ETag / If-None-Match system only kicks in after the TTL has already expired. If the TTL is still valid, the browser never sends the conditional request. Your file could have completely changed on the server, and the user would never know.

When a user presses Ctrl+Shift+R (or Cmd+Shift+R on Mac), the browser sends Cache-Control: no-cache in the request and skips its local cache entirely. It gets a fresh response, and yes, the cache is updated with the new file.

But this is a developer tool, not a user solution. Consider the problems: Your real users don't know what a hard refresh is Even if they did, you can't instruct every user to do it after every deploy After the hard refresh, the heuristic TTL starts again from near-zero and will silently grow again over time Your next deploy will run into the same problem

Want to discuss this further?

Book a free strategy call with our team to see how these insights apply to your specific business goals.

Book a consultation