Your PWA Still Shows the Old Version After Deploy


You deploy a fix. You reload. It’s there. Someone messages you an hour later with a screenshot of the bug you just fixed.

You tell them to hard-refresh. Sometimes that works, which is the worst possible outcome, because now you believe hard-refreshing is the fix.

It isn’t. Your service worker is answering the request before the network ever hears about it.

What’s actually happening

A service worker installed by your PWA sits between the browser and the network. On a cache-first strategy — which is what most starter service workers use, and what makes a PWA work offline — the flow is:

  1. Page asks for app.js
  2. Service worker checks its cache, finds app.js, returns it
  3. The network is never contacted

Your new app.js is sitting on the server, perfectly deployed, and nobody is asking for it. Ctrl+Shift+R sometimes bypasses it and sometimes doesn’t, depending on the browser and what’s being requested. On an installed PWA with no visible address bar, the user has no way to hard-refresh at all.

The fix

The service worker only replaces its cache when the cache name changes. So the cache name has to change on every deploy:

// sw.js
const CACHE_NAME = 'app-v37';   // <-- this number must go up every single deploy

self.addEventListener('activate', (e) => {
  e.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
    )
  );
});

Bump v37 to v38, deploy, and on next visit the worker installs a fresh cache and deletes the old one.

That’s the whole fix. Which is exactly why it keeps happening — it’s one character, it’s in a file you rarely open, and forgetting it produces no error. The deploy succeeds. The site is live. The old version is what people see.

Don’t rely on remembering

I forgot it enough times to stop trusting myself, so the version bump is no longer a thing I do. It’s a step in the deploy script:

# bump the service worker cache version, then deploy
n=$(grep -oE "app-v([0-9]+)" sw.js | head -1 | grep -oE "[0-9]+")
sed -i "s/app-v$n/app-v$((n+1))/" sw.js

If you’d rather not touch the file automatically, at least make the deploy refuse to run when the version is unchanged from the last deployed copy. A deploy that stops and says “you didn’t bump the cache” is worth more than a checklist item, because the checklist is only read by someone who already remembered.

How to confirm it actually worked

Not by reloading your own browser — yours is the one machine most likely to be clean.

Open DevTools → Application → Service Workers on a device that had the old version, and check that the active worker’s cache name matches what you just deployed. Or check from outside the browser entirely:

curl -s https://your-site/sw.js | grep -oE "app-v[0-9]+"

That tells you what the server is handing out. If it matches your new number and a user still sees the old page, they simply haven’t revisited yet — the worker updates on the next navigation, not instantly.

The bit that generalizes

A stale PWA is a specific instance of a general shape: the system is working exactly as designed, and the design’s success mode is indistinguishable from failure. Offline-capable caching and “serving a stale build” are the same behavior seen from two directions.

Whenever a feature’s correct behavior can look identical to a bug, you need a signal that isn’t the feature itself. Here it’s the version string, checked from outside the browser doing the caching.

Comments

Loading comments…