Skip to content

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().

  • 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 activate succeeds, 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.
  • Register. navigator.serviceWorker.register(url, { scope }) points the page at a worker script. Registration is idempotent and bound to a scope.
  • Install. The install event fires once per worker version — the place to pre-cache the app shell with event.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 activate event fires when the worker takes over — the place to clean up old caches. Until activation, the worker does not handle fetch.
  • 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 / PlatformSupportSinceConfidenceSourceNotes
Chrome (Android)✅ yes40highref
Chrome (Desktop)✅ yes40highref
Edge (Desktop)✅ yes17highref
Safari (iOS)✅ yes11.3highrefStorage may be evicted after prolonged non-use.
Safari (macOS)✅ yes11.1highref
Firefox (Desktop)✅ yes44highref
Samsung Internet✅ yes4.0highref

Source: spec · MDN · Last verified 2026-06-24 · Confidence: high

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().
  • Pre-cache the app shell in install inside event.waitUntil(...).
  • Delete stale caches in activate inside event.waitUntil(...).
  • Decide your update policy: default waiting (safe) vs skipWaiting() (immediate).
  • If you use skipWaiting(), also handle controllerchange to 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.

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.