# The service worker update flow and skipWaiting

> How the browser detects a new service worker version, what the waiting state means, and how skipWaiting() and clients.claim() opt into an immediate takeover.

**In one line:** When the browser finds a byte-different service worker script, it installs the new version alongside the old one and holds it in the *waiting* state until every controlled tab closes — unless you call `self.skipWaiting()` to bypass that hold.

## How the browser detects an update

The browser re-fetches the worker script automatically on every navigation (within scope) and on any explicit `registration.update()` call. If the fetched script differs by even one byte from the currently registered one, the browser starts a new install.

The update check itself uses a special freshness rule: the worker script bypasses the HTTP cache for checks older than 24 hours, even if the server has sent aggressive `Cache-Control` headers. The `updateViaCache: 'none'` registration option makes every check bypass the cache entirely.

## The waiting state

After the new worker installs successfully (its `install` event resolves), it enters *waiting*. It will not activate — and therefore will not handle `fetch` events — until:

1. All tabs controlled by the *old* worker are closed, **or**
2. The new worker calls `self.skipWaiting()`.

This design guarantees that a page and the worker serving it are always from the same version. A tab running old HTML is never handed to a new worker that may have different cache keys or API assumptions.

## `self.skipWaiting()`

Calling `skipWaiting()` inside the `install` event tells the browser to activate the new worker immediately, even if the old one still controls open tabs:

```js
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('app-v2').then((cache) => cache.addAll(['/index.html', '/app.js']))
  );
  self.skipWaiting(); // activate immediately after install
});
```

`skipWaiting()` is asynchronous but its effect is usually immediate relative to the current task. Calling it outside `install` is also valid but the most reliable place is inside `waitUntil`.

**Risk:** If the new worker's cached resources are incompatible with the HTML already loaded by the old worker, calling `skipWaiting()` without reloading all tabs can cause runtime errors. Always pair it with a `controllerchange` listener.

## `clients.claim()`

By default a newly activated worker does not control pages that were already open when it was registered — they run without a service worker for their current lifetime. `clients.claim()` lets the new worker adopt those pages immediately:

```js
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(keys.filter((k) => k !== 'app-v2').map((k) => caches.delete(k)))
    )
  );
  self.clients.claim(); // take control of all open tabs in scope
});
```

Use `clients.claim()` only when you have a clear reason — for example, ensuring the very first page load after registration is controlled (so offline works immediately). Unnecessary use of `claim()` causes all currently-open tabs to swap workers mid-session.

## Notifying the user

When `skipWaiting()` activates a new worker, existing tabs are still running old code. The recommended pattern is to listen for `controllerchange` in the page and prompt for a reload:

```js
// In the page
let refreshing = false;
navigator.serviceWorker.addEventListener('controllerchange', () => {
  if (refreshing) return;
  refreshing = true;
  window.location.reload();
});
```

Alternatively, use the `registration.waiting` property to detect a pending update and show a "reload for update" UI instead of forcing an automatic reload.

## Manual update check

Call `registration.update()` to trigger an immediate check for a new worker script:

```js
navigator.serviceWorker.ready.then((registration) => {
  registration.update();
});
```

The raw Service Workers API requires you to call `registration.update()` yourself for proactive update detection. If using Workbox, its `workbox-window` package provides `wb.addEventListener('waiting', ...)` for reacting to updates, but periodic re-checking still requires an explicit call to `wb.update()` or the browser's own navigation-triggered check.

## Practical checklist

- [ ] Use `updateViaCache: 'none'` when registering to ensure the browser always re-fetches the worker script.
- [ ] If you use `skipWaiting()`, also add a `controllerchange` listener in the page to reload tabs and avoid version mismatches.
- [ ] Clean up old cache names in the `activate` event after `skipWaiting()` ensures the new worker is active.
- [ ] Consider a visible "Update available — click to reload" UI rather than silent auto-reload for critical apps.
- [ ] Test the update flow by loading the page, changing the worker, then navigating (not just refreshing in DevTools).

## Cross-references

- [Service worker lifecycle](/reference/service-worker/lifecycle/) — full install / activate / waiting flow
- [Registration and scope](/reference/service-worker/registration-scope/) — `updateViaCache` option on `register()`
- [Debugging service workers](/reference/service-worker/debugging/) — use DevTools to force updates and inspect waiting workers