Your update never reaches installed users
Two separate things cause this. The everyday one: a service worker only takes control on the next navigation, so a user with the app open keeps the old version until they fully close and reopen it — and if you never call skipWaiting, the new worker waits indefinitely. The permanent one: changing start_url without a pinned id makes the browser treat your app as a different app, so everyone who already installed keeps the old one forever and silently stops receiving anything.
The waiting worker
When you deploy, the browser fetches your new service worker and installs it — then parks it in a waiting state. The old worker is still controlling every open tab, and it stays in charge until every tab of your app is closed. For an installed PWA that a user leaves open for days, that moment may never come.
self.skipWaiting() in the install handler takes control immediately. It is the right default for most apps, with one caveat worth knowing: a page that is mid-session gets a worker serving different assets than the ones it loaded with, which can break a running app. The considered version is to call skipWaiting only after asking the user, which is what the update-prompt pattern does.
The one that cannot be undone
This is the serious one, and almost nobody checks it. Your app's identity is derived from start_url unless you pin an id in the manifest. Change start_url — move from / to /app, add a tracking parameter, switch domains — and the browser concludes this is a different application. Users who installed the old one keep it. It points at a path you may no longer serve. They receive no updates and see no error.
We read the manifests of 386 apps in this directory on 12 August 2026. 329 of the 373 readable ones declare no id at all — 88%, one of several defects we found across the ecosystem. Every one of them is one URL change away from orphaning its installed users, and would find out only from a support message that never comes.
Pin it now, while it costs nothing: "id": "/" in the manifest. Setting id on an app that already has installs does not break them, as long as it matches the current start_url path. Setting it later, after you have already moved, does not bring the orphans back.
A trap worth naming: id is not a redirect
We found a live example while measuring: an app whose id was /?source=pwa while start_url was /. The intent was clearly to attribute launches from the installed app in analytics. It measures nothing. id is only ever an identity key — the browser never navigates to it. The parameter has to be on start_url to appear in a single pageview.
The failure mode is silence in both directions: the identity works, the analytics show nothing, and there is no error anywhere to suggest the two facts are related.
Frequently asked
You cannot, and that is by design. What you can do is call skipWaiting so the next launch takes the new version, and ship an update prompt so users choose the moment. Anything more aggressive risks swapping assets under a running session.
Not reliably. Icons are cached by the OS at install time, and Android in particular is slow to refresh them. Users who reinstall get the new icon; the rest may keep the old one for a long time regardless of what your manifest says.
Apps mentioned
Keep reading
592+ Progressive Web Apps, each checked by hand — installable from your browser, no app store.
Browse the directory →