Skip to content

Service Worker Lifecycle

Every service worker goes through a strictly defined state machine managed by the browser. Understanding this lifecycle is essential — skip it and your updates silently fail to activate, or you break pages that are still running the old worker.

stateDiagram-v2
  [*] --> Parsed : browser parses sw.js
  Parsed --> Installing : install event fires
  Installing --> Installed : installEvent.waitUntil() resolves
  Installing --> Redundant : waitUntil() rejects
  Installed --> Activating : no old SW active, or skipWaiting()
  Installed --> Waiting : old SW still controlling pages
  Waiting --> Activating : all old-SW tabs closed, or skipWaiting()
  Activating --> Activated : activateEvent.waitUntil() resolves
  Activating --> Redundant : waitUntil() rejects
  Activated --> Redundant : replaced by a newer SW
  Waiting --> Redundant : replaced by a newer SW
Service worker lifecycle state machine

The install event fires the moment the browser has parsed and downloaded your service worker file. This is the ideal time to pre-cache the resources your app needs for offline use.

const CACHE_NAME = 'v1';
const PRECACHE_URLS = ['/', '/styles.css', '/app.js'];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_URLS))
);
});

event.waitUntil() accepts a Promise. If the Promise resolves, the SW moves to the Installed state. If it rejects (e.g., a network error while pre-caching), the SW moves to Redundant and is discarded — nothing breaks for the user; the old SW keeps running.

After installation, the new SW sits in Waiting if any tab is still controlled by the previous version. The browser will not activate the new SW until every tab running the old one is closed (or navigated away from).

To skip the waiting period programmatically, call self.skipWaiting() inside the install handler:

self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('v1').then((cache) => cache.addAll(['/'])).then(() => self.skipWaiting())
);
});

Once the old SW is gone (or skipWaiting() was called), the new SW fires its activate event. This is the right time to clean up old caches so stale assets do not consume storage indefinitely.

const CURRENT_CACHES = ['v2'];
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((names) =>
Promise.all(
names
.filter((name) => !CURRENT_CACHES.includes(name))
.map((name) => caches.delete(name))
)
).then(() => self.clients.claim())
);
});

self.clients.claim() makes the newly activated SW take control of all open pages immediately, without waiting for the next navigation.

StateWhat happensWhat you do
InstallingSW is being set upPre-cache assets in install
WaitingNew SW is ready but held backOptionally call skipWaiting()
ActivatingNew SW is taking overDelete old caches in activate
ActivatedSW is in full controlServe requests via fetch event
RedundantSW was replaced or failedNothing — browser disposes of it
What happens if the Promise passed to event.waitUntil() in the install handler rejects?
Why does a new service worker sit in the Waiting state?
What is the purpose of self.clients.claim() in the activate handler?
Which lifecycle state is the right time to delete stale caches from previous SW versions?