Service worker lifecycle: install, activate, and update
In one line: A service worker moves through register → install → (wait) → activate
before it controls pages. A new version installs in the background but stays waiting
until every tab using the old one closes — so updates are atomic and never half-applied,
unless you opt into skipWaiting() and clients.claim().
Why the lifecycle exists
Section titled “Why the lifecycle exists”- Atomic updates. A page’s HTML, JS, and the service worker that caches them are versioned together. By holding a new worker in waiting until old clients are gone, the browser avoids a tab running new code against an old cache (or vice versa).
- Offline safety. The worker only takes control after
activatesucceeds, so a broken install never replaces a working offline copy. The previous worker keeps serving until the new one is fully ready. - No surprise takeovers. A freshly installed worker does not control already-open tabs by default. Existing pages keep their current worker so behavior stays stable for the duration of a session.
The phases
Section titled “The phases”- Register.
navigator.serviceWorker.register(url, { scope })points the page at a worker script. Registration is idempotent and bound to a scope. - Install. The
installevent fires once per worker version — the place to pre-cache the app shell withevent.waitUntil(...). Install failing discards the worker. - Waiting. If an active worker already controls clients, the new (installed) worker enters waiting. It will not activate until all tabs controlled by the old worker close. This is the classic “the update is stuck until I close every tab” behavior.
- Activate. The
activateevent fires when the worker takes over — the place to clean up old caches. Until activation, the worker does not handlefetch. - Idle / terminated. Between events the browser may stop the worker to save memory and restart it on the next event. Never rely on in-memory state across events.
Browser & ecosystem support
Section titled “Browser & ecosystem support”| Browser / Platform | Support | Since | Confidence | Source | Notes |
|---|---|---|---|---|---|
| Chrome (Android) | ✅ yes | 40 | high | ref | — |
| Chrome (Desktop) | ✅ yes | 40 | high | ref | — |
| Edge (Desktop) | ✅ yes | 17 | high | ref | — |
| Safari (iOS) | ✅ yes | 11.3 | high | ref | Storage may be evicted after prolonged non-use. |
| Safari (macOS) | ✅ yes | 11.1 | high | ref | — |
| Firefox (Desktop) | ✅ yes | 44 | high | ref | — |
| Samsung Internet | ✅ yes | 4.0 | high | ref | — |
Controlling the takeover
Section titled “Controlling the takeover”| Goal | Mechanism | Implication |
|---|---|---|
| Apply updates only after all tabs close (safest) | Do nothing — default waiting behavior. | Users get the new version next time they fully restart the app; no mid-session swap. |
| Activate a new worker immediately | Call self.skipWaiting() in install. |
Skips waiting; pair with care so the new worker’s cache matches loaded pages. |
| Take control of existing uncontrolled tabs | Call self.clients.claim() in activate. |
The first page load after registration gets controlled without a reload. |
| Force an update check | registration.update(), or a normal reload. |
The browser re-fetches the worker script; a byte-different script installs a new version. |
| Reload all tabs on a new worker | Listen for controllerchange, then location.reload(). |
Avoids new-worker/old-page mismatches after skipWaiting() + claim(). |
Practical checklist
Section titled “Practical checklist”- Pre-cache the app shell in
installinsideevent.waitUntil(...). - Delete stale caches in
activateinsideevent.waitUntil(...). - Decide your update policy: default waiting (safe) vs
skipWaiting()(immediate). - If you use
skipWaiting(), also handlecontrollerchangeto avoid mixed versions. - Use
clients.claim()only if you need the first load to be controlled without a reload. - Keep the worker script byte-stable between deploys you do not want to ship — any byte change triggers a new install.
- Treat the worker as stateless; persist anything durable in Cache Storage or IndexedDB.
What this means for trust
Section titled “What this means for trust”The lifecycle is what makes a PWA feel as reliable as a native app: updates land cleanly or not at all, and the offline experience never breaks mid-update. Understanding waiting explains the most common confusion — “I deployed, but users still see the old version” — and lets you choose deliberately between a safe, eventual rollout and an immediate one.