The service worker lifecycle

The service worker lifecycle is the most complicated part of the API. It can feel as though it's working against you until you understand what it's trying to achieve. Once that's clear, the lifecycle enables seamless, unobtrusive updates that combine the strengths of web and native patterns. This deep dive covers the mechanics section by section.

What the lifecycle is trying to do

The lifecycle has four main goals:

  • Make offline-first possible.
  • Let a new service worker prepare itself without disrupting the current one.
  • Ensure an in-scope page is controlled by the same service worker (or none) throughout its lifetime.
  • Guarantee only one version of your site runs at a time.

The last point is especially critical. Without service workers, a user can open one tab, then another, resulting in two versions of your site running simultaneously. That's often harmless, but when shared storage is involved, two tabs can have conflicting opinions about how to manage it—leading to errors or data loss.

The first service worker

Key points up front:

  • The install event is the first event a service worker receives, and it fires only once.
  • A promise passed to installEvent.waitUntil() signals the duration and success or failure of the install.
  • A service worker won't receive events like fetch and push until it has finished installing successfully and becomes active.
  • By default, a page's fetches won't go through a service worker unless the page request itself did. You need to refresh the page to see the service worker's effects.
  • clients.claim() overrides that default and takes control of uncontrolled pages.

Consider this HTML, which registers a service worker and adds an image of a dog after three seconds:

<!DOCTYPE html>
An image will appear here in 3 seconds:
<script>
  navigator.serviceWorker.register('/sw.js')
    .then(reg => console.log('SW registered!', reg))
    .catch(err => console.log('Boo!', err));

  setTimeout(() => {
    const img = new Image();
    img.src = '/dog.svg';
    document.body.appendChild(img);
  }, 3000);
</script>

The accompanying service worker, sw.js, caches an image of a cat and serves it for any request to /dog.svg:

self.addEventListener('install', event => {
  console.log('V1 installing…');

  // cache a cat SVG
  event.waitUntil(
    caches.open('static-v1').then(cache => cache.add('/cat.svg'))
  );
});

self.addEventListener('activate', event => {
  console.log('V1 now ready to handle fetches!');
});

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  // serve the cat SVG from the cache if the request is
  // same-origin and the path is '/dog.svg'
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/cat.svg'));
  }
});

When you run the example, you'd expect the cat image to appear on the first load, given that the image request happens well after registration. In practice, you'll see the dog initially. After a refresh, the cat appears. The reason lies in scope, control, and timing.

Scope and control

A service worker registration's default scope is ./ relative to the script URL. Registering at //example.com/foo/bar.js yields a default scope of //example.com/foo/.

Pages, workers, and shared workers are all called clients. A service worker can only control clients in its scope. Once a client is controlled, its fetches route through the in-scope service worker. You can check control status with navigator.serviceWorker.controller, which returns either a service worker instance or null.

Download, parse, and execute

The first service worker downloads when you call .register(). If the script fails to download, fails to parse, or throws during initial execution, the register promise rejects and the worker is discarded. Chrome's DevTools reports the error in the console and in the service worker section of the Application tab:

Error displayed in service worker DevTools tab

Install

The install event fires as soon as the worker executes, and only once per service worker. Any change to the script makes the browser treat it as a new service worker with its own install event. The promise passed to event.waitUntil() tells the browser when installation completes and whether it succeeded.

If that promise rejects, the install is considered failed and the browser discards the worker permanently—it will never control clients. This is why you can rely on resources cached during install being present in your fetch handlers: they are dependencies, not optimizations.

Activate

Once a service worker is ready to control clients and handle functional events like push and sync, it receives an activate event. Activation, however, does not mean the page that called .register() is now controlled.

In the demo, even though dog.svg is requested long after the service worker activates on first load, the request bypasses the worker and shows the dog. This default favors consistency: if a page loads without a service worker, its subresources also load without one. On a refresh, the page becomes controlled, and both the page and the image go through fetch events—yielding the cat.

Jake Archibald

clients.claim

Calling clients.claim() from within the service worker after activation takes control of uncontrolled clients. A variation of the demo that calls clients.claim() in its activate handler shows the cat on first load—if the timing works out. The service worker must activate and call clients.claim() before the image request fires.

This approach can be risky. If your service worker serves pages differently than the network would, clients.claim() may end up controlling clients that were never meant to be controlled from the start.

How a Changed Service Worker Script Gets Applied

When you modify a service worker script, the browser doesn't immediately replace the running version. The update process is decoupled from the initial registration, with a defined sequence that ensures users always have a consistent experience.

An update check happens:

  • On a navigation to an in-scope page.
  • On functional events like push and sync, unless a check already occurred within 24 hours.
  • When calling .register(), but only if the service worker URL has changed.

Most browsers, including Chrome 68 and later, ignore HTTP caching headers for the service worker script when checking for updates, but still respect them for resources fetched via importScripts(). You can override this default behavior with the updateViaCache option at registration.

When the fetched script is byte-different, the new worker is installed alongside the existing one and gets its own install event. If the new script has a non-ok status (such as 404), fails to parse, throws during execution, or rejects during install, the new worker is discarded and the current one continues to operate.

Install and Wait

In the install handler for the new version, be careful with cache naming. Using a version-specific cache like static-v2 allows you to set up new assets without clobbering what the old worker is still using.

After installing successfully, the new worker delays activation until the old worker is no longer controlling any clients. This "waiting" state ensures that only one version of your worker runs at a time. Merely refreshing the page won't trigger the takeover. During a navigation, the current page won't leave until the response headers arrive, so a refresh still leaves the old worker controlling a client. To see the update in practice, you must close or navigate away from all tabs using the current worker.

Activate for Cleanup

Activation happens when the old worker has been dismissed and the new one can take control. This is the proper time to migrate data or clear caches. The activate event lets you remove stale caches; passing a promise to event.waitUntil() will buffer functional events like fetch, push, and sync until that cleanup is finished.

Handling the Waiting Phase

If you don't need the guaranteed atomic switchover, you can call self.skipWaiting() to activate as soon as installation has concluded. This promotes the worker from waiting to active, but be aware the takeover is subject to race conditions with concurrent page activity. It's common to call skipWaiting() during install, though some designs trigger it via postMessage() after user interaction.

The browser also allows manual update checks, which can be triggered by calling the update() method on your registration. For long-lived pages where users don't reload often, a periodic interval can help drive fresh workers.

Service Worker Scripts Need a Stable URL

Versioning the URL of your worker script — such as registering sw-v1.js then sw-v2.js — creates a trap. The old service worker continues serving the cached, older page. Users will never fetch the new HTML that points to your new worker. You end up needing a new worker to serve the new page that requires the new worker to run. Updating the script in place remains the correct approach.

Tools for Debugging

The strict lifecycle is user-centric, but makes development harder. DevTools and one spec behavior ease the pain:

Update on Reload

The "Update on Reload" option alters the process: each navigation refetches the worker script, installs it as a new version even if byte-identical, skips waiting, and then lets your worker activate. You don't need to close tabs to see your changes take effect.

Skip Waiting from DevTools

You can manually promote an installed worker from the waiting state to active using the "skip waiting" action in the Application panel.

Shift-Reload for Clean Tests

A force reload (Shift+Reload) bypasses the service worker to render a page off the network. This is part of the spec, so it's available in all service-worker-supporting browsers.

The Whole Cycle Is Observable

Rather than committing you to one rigid strategy, the lifecycle implementation exposes each step for inspection. This lets you build updates that match your own audience's needs rather than relying on assumptions baked into the API.

A clear understanding of this chain — install, wait, activate, skip — transforms service worker updates from an opaque barrier into a mechanism you can bend to your release schedule.