How to Handle Service Worker Updates with skipWaiting

This guide covers the update moment in Service Worker Caching Strategies, within Advanced Caching Strategies & CDN Architecture. When you deploy a new service worker, browsers install it in the background but, by default, do not activate it while any tab is still controlled by the old one. That protects running pages from having their worker — and its caches — swapped mid-session. It also means users who keep a tab open can run an old release for days.

self.skipWaiting() lets the new worker activate immediately. Combined with clients.claim(), it takes control of open pages at once. That is fast, and it risks version skew: the page's JavaScript is from the old release while the worker (and its caches) is from the new one. The right policy depends on how tolerant your app is of mixed versions and how important it is for users to get updates promptly.

Three update policies Comparison of waiting for tabs to close, prompting the user, and skipping waiting automatically. Three update policies Default wait / prompt • New worker waits until tabs close • Or user clicks Refresh in a prompt • No mixed versions mid-session • Slow rollout for long-lived tabs skipWaiting + claim • New worker controls pages at once • Fastest rollout • Old page JS + new worker caches • Needs version-tolerant design

Rapid Diagnosis

  • Check DevTools after a deploy. A worker "waiting to activate" indicates the default policy; check how long it waits in practice.
  • Measure rollout. Send the worker's version in RUM and chart the share of sessions on the latest version after each deploy.
  • Look for skew bugs. Chunk load errors, missing cached assets or API shape mismatches shortly after deploys suggest skipWaiting without safeguards.
  • Check the update UX. Do users ever learn a new version exists?

Root Cause Analysis

1. Long-lived tabs block activation. Web apps left open all day never let the new worker activate under the default policy.

2. Unsafe skipWaiting. Activating immediately with cache cleanup removes assets the old page still needs.

3. No update signal. The app has no way to tell the user or itself that a new version is ready.

4. Reload loops. Naive "reload on controllerchange" code can reload repeatedly or at disruptive moments.

Prompted update flow Sequence of a new service worker installing and waiting, the page showing a refresh prompt, and the user accepting so the worker skips waiting and the page reloads. Prompted update flow New worker Page User installed (waiting) Update available — refresh? clicks Refresh SKIP_WAITING message controllerchange → reload

Step-by-Step Resolution

1. Detect a waiting worker in the page

javascript
const reg = await navigator.serviceWorker.register('/sw.js');
function onWaiting(worker) { showUpdateBanner(() => worker.postMessage({ type: 'SKIP_WAITING' })); }
if (reg.waiting) onWaiting(reg.waiting);
reg.addEventListener('updatefound', () => {
  const w = reg.installing;
  w?.addEventListener('statechange', () => { if (w.state === 'installed' && navigator.serviceWorker.controller) onWaiting(w); });
});
// trade-off: the banner interrupts users slightly. Make it non-modal and
// remember dismissals for the session so it does not nag.

2. Skip waiting only on request

javascript
// sw.js
self.addEventListener('message', (event) => {
  if (event.data?.type === 'SKIP_WAITING') self.skipWaiting();
});

3. Reload once when the new worker takes control

javascript
let reloading = false;
navigator.serviceWorker.addEventListener('controllerchange', () => {
  if (reloading) return;
  reloading = true;
  location.reload();
});
// trade-off: reloading discards unsaved state. Save drafts first, or defer the
// reload until the next navigation for apps with long editing sessions.

Expected outcome: users get updates when they choose, with one clean reload and no mixed versions.

4. If you skip waiting automatically, make the app version-tolerant

Keep old hashed assets on the server (so old pages can still load lazy chunks), avoid deleting old caches until clients have reloaded, and keep APIs backward compatible for at least one release. Then skipWaiting + clients.claim() is safe for apps where prompt rollout matters more than strict version isolation.

Choosing an update policy Decision sequence for choosing between default waiting, prompting and automatic skipWaiting. Choosing an update policy Users keep tabs open for hours (web app)? Prompt to refresh (skipWaiting on consent) yes no Critical security or data fix? skipWaiting + claim + forced reload yes no App tolerant of mixed versions? Automatic skipWaiting at next navigation yes no Default — activate when tabs close

Verification

Deploy to staging with a tab open: the banner should appear after the new worker installs, clicking it should reload once into the new version, and Cache Storage should then hold only the new version's caches. Chart version adoption in RUM after real deploys; prompted updates typically reach most active users within a day, versus days for the default policy in long-session apps.

Worked Example: A Project Management App

A project management web app used the default policy; with users keeping tabs open for days, a fix for a data-loss bug took over a week to reach 90% of sessions. The team added a waiting-worker banner with a "Refresh" button, auto-saved drafts before reloading, and reserved forced skipWaiting for critical releases. Typical releases reached 85% of active sessions within 24 hours, and a later critical fix reached 98% within an hour using the forced path.

Coordinating Updates With API Changes

Service worker updates interact with backend deploys. If the API changes shape, pages running the old release (under either policy) may call endpoints that no longer behave as expected. Keep API changes backward compatible for at least as long as old clients remain active — RUM version adoption tells you how long that is — or version the API so old clients keep calling the old version. Tie the service worker's version, the app's build ID and the API's compatibility window together in release planning, so an update that is safe for the worker is also safe for the data it serves.

Testing the Update Path in CI

Automate the scenario that causes most update bugs. In Playwright, load the app with version A of the worker, then serve version B (by switching a build directory or a header the test server reads), trigger registration.update(), and assert: the waiting state is detected, the banner appears, accepting it produces exactly one reload, the page then reports version B, and Cache Storage contains only B's caches. A second test should keep an old tab open across the update and confirm it can still navigate and lazy-load chunks.

Common Mistakes

  • Calling skipWaiting in install unconditionally. Every deploy then swaps workers under running pages.
  • Reload loops. Missing guards on controllerchange cause repeated reloads.
  • Cleaning caches before clients reload. Old pages lose assets they still need.
  • Never testing updates. Most service worker bugs live in the update path.

Edge Cases

Multiple tabs. Accepting the update in one tab reloads that tab; other tabs are now controlled by the new worker but run old code. Broadcast a message so they can reload too.

Workbox Window. The workbox-window package wraps this flow (messageSkipWaiting, waiting events) and handles many edge cases.

Navigation preload state. New workers must enable navigation preload again on activate if you use it.

Users who never click. The default activation on tab close still applies, so updates eventually roll out.

FAQ

Is skipWaiting dangerous?

Only when the app cannot tolerate old page code running alongside a new worker. With retained assets, compatible APIs and cache cleanup after reloads, it is safe and gives the fastest rollout.

What does clients.claim do?

It makes the newly activated worker control already-open pages immediately (otherwise they stay uncontrolled until navigated). It is commonly paired with skipWaiting.

How often does the browser check for a new worker?

On navigations within scope and on functional events, with HTTP caching of the script capped at 24 hours. Calling registration.update() periodically (for example hourly in long-lived apps) checks sooner.

Should content sites prompt for updates?

Usually not. Visitors navigate between pages often, and network-first HTML shows new content anyway; the default policy is fine. Prompts suit long-lived application sessions.

Can I update silently at the next navigation?

Yes: when a worker is waiting, intercept the next in-app navigation, post SKIP_WAITING, and let the navigation load the new version. Users never see a prompt, and no work is lost.

Does the update policy affect performance metrics?

Indirectly. Faster rollout delivers performance fixes sooner; forced reloads add an extra navigation for affected users. Track version adoption alongside your Core Web Vitals to see when improvements actually reach users.