◉ANTIGRAVITY LABJP
Articles/App Development
▣ App Development/2026-10-15Intermediate

I Swapped an Immutable Asset at the Same URL, and Only the Browser Kept the Old Copy

Overwrite a long-cached JS file at the same URL and neither a CDN purge nor a refresh will make the browser fetch it again. Four delivery styles — same URL, no-cache, query string, content-hash filename — compared in Chromium, plus a complete build-step script that adds the hash.

cachingCache-ControlCloudflare4buildindie developer15Antigravity382

✦ Premium Article

I had finished a build and replaced a JS file under the same name. The CDN entry for that URL had been purged, so a fresh copy should have been on its way. In my own browser, though, the old behavior kept running.

The purge had probably worked. What hadn't moved was the layer one step closer to the reader — the browser itself. I had stepped on the other side of a rule I'd set for myself: give assets a long cache lifetime, and overwrite them in place.

Let me give you the conclusion first. If the content changes at the same URL, you can't give it a long cache lifetime. If you give it a long cache lifetime, the URL has to change when the content does. Below, I line that rule up against four delivery styles, show what Chromium actually did in my tests, and leave the script I now run at the end of the build.

Separate the layers first

When "the old one is still showing," three places could be holding it: the CDN edge, the reader's browser, and the URL your HTML points at. A purge reaches only the edge. It never touches the browser's cache.

Two lines in the browser console are enough to tell them apart. Fetch the same URL once with the cache allowed and once with it forbidden.

// What this solves: tell whether the browser, or something further upstream, holds the old copy
const u = '/assets/app.js'; // replace with the URL you suspect
const a = await (await fetch(u, { cache: 'default' })).text(); // may use the browser cache
const b = await (await fetch(u, { cache: 'reload' })).text();  // always goes to the server
console.log({ same: a === b, defaultHead: a.slice(0, 60), reloadHead: b.slice(0, 60) });
// Why it works: same === false means the browser is holding the old copy.
// If same === true and it's still old, the edge or origin has it (check with curl -I separately).

That cache option is what I used to separate the layers. The moment default returned the old file and reload returned the new one, the cause was the browser, not the CDN — and every extra purge was wasted effort.

Four delivery styles, side by side

Here is how each style should behave when you swap the asset, on paper.

StyleURLCache-ControlDoes the swap reach readers?
A Same URL, long + immutable/assets/app.jspublic, max-age=31536000, immutableNo
B Same URL, long only/assets/app.jspublic, max-age=31536000No
C Same URL, always revalidate/assets/app.jsno-cache + ETagYes (asks the server each time)
D Query string/assets/app.js?v=2long + immutableYes, once the HTML is fresh
E Content-hash filename/assets/app.3f9a1c2e.jslong + immutableYes, once the HTML is fresh

That's the expectation. Now let me see what a real browser does.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦When a swapped asset still behaves like the old one, you'll be able to tell whether the CDN, the browser, or the HTML is holding it with two fetch calls using the cache option
✦You'll be able to choose among same-URL, no-cache, query-string, and content-hash delivery after checking each one yourself in a local Chromium, instead of trusting a table
✦You'll skip the re-investigation of 'I purged it and it's still old' on every release by running one hash-the-filenames script at the end of the build
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

▣ App Dev2026-09-29
Purging Cloudflare Didn't Bring Back the New Wallpapers: the Stale JPEGs Were Held by XPageSpeed on the Origin
An operations record from a wallpaper app: images overwritten at the same URL kept arriving stale even after a Cloudflare purge. Covers the three-way local/origin/edge probe that names the guilty layer, when prefix purge beats per-URL purge, and how to switch off Xserver's XPageSpeed in .htaccess without a 500.
▣ App Dev2026-09-19
I Asked for the Whole Quote PDF Tool at Once, Then Split It Into Four Requests
How I moved my client quote paperwork from copy-and-edit to a small PDF tool built with Antigravity, why the first single-shot request had to be thrown away, and the four smaller requests that replaced it, with working code.
▣ App Dev2026-09-03
A dependency without musllinux wheels doesn't fail on Alpine. It just gets slower
On Alpine, a dependency without musllinux wheels can quietly fall back to pure Python. Protobuf went from 0.043ms to 36.2ms, and pip reported no error.