Skip to content

Update strategies

Published

Outcome: a considered choice for how a new service worker version reaches your users, instead of an accidental default. The mechanism (skipWaiting(), clients.claim(), the waiting state) is covered in the update flow reference; this guide is about which strategy to pick and why.

Without calling skipWaiting(), a newly installed worker sits in the waiting state until every tab controlled by the old worker closes. The default favors consistency: a page that loaded without a service worker won’t suddenly get one mid-session, and a page loaded under one worker version won’t be handed to a different version’s cache assumptions. For most apps, this quiet default is the right choice — but a reload alone isn’t guaranteed to apply the update: during a reload, the old and new page’s clients can briefly overlap even with a single tab, so the waiting worker only takes over once every tab controlled by the old worker has actually closed or navigated away.

Calling self.skipWaiting() in the install event causes the new worker to kick out the current active worker and activate itself as soon as it enters the waiting phase, even while it’s still controlling open tabs. It’s common to call it directly in the install handler:

self.addEventListener('install', (event) => {
self.skipWaiting();
});

The trade-off: your new service worker is then likely controlling pages that were loaded with an older version, which can break things if the new worker’s cached resources are incompatible with the HTML already on the page. The guidance is direct — if this might break things, don’t use skipWaiting() unconditionally.

clients.claim() is a related, separate decision: it lets a newly activated worker adopt pages that were already open when it was registered. It only really matters on the very first load, since a page usually works fine without a service worker for that first visit thanks to progressive enhancement — so treat it as situational rather than routine boilerplate.

A middle ground defers skipWaiting() to a user-driven moment instead of calling it unconditionally in install: the new worker installs and waits, the page shows the user an “update available” notice, and only on user action does the page postMessage() the waiting worker to trigger skipWaiting(), followed by a controlled reload.

// In the page: listen for a controller change and reload once, after the user
// has agreed to update.
let refreshing = false;
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (refreshing) return;
refreshing = true;
window.location.reload();
});

This pattern — covered in full in the update flow reference — avoids both the silent inconsistency risk of unconditional skipWaiting() and the “update never applies until the user closes every tab” cost of the pure default.

The browser checks for a new worker script automatically on navigation to an in-scope page, and on functional events like push and sync (unless a check already happened within the previous 24 hours). If you expect a user to keep a tab open for a long time without navigating or reloading, you may want to call registration.update() on an interval, such as hourly, so a long-lived session still discovers new versions in a reasonable time:

navigator.serviceWorker.register('/sw.js').then((reg) => {
setInterval(() => reg.update(), 60 * 60 * 1000);
});
Situation Recommended strategy
Typical app; users eventually close or navigate away from all tabs Default (no skipWaiting()) — simplest, most consistent.
Users keep tabs open for hours/days Default plus a periodic registration.update() check, so updates are discovered even without a fresh navigation.
Update carries a breaking cache/schema change Default, or the user-prompted reload — never unconditional skipWaiting(), since it can control old pages with incompatible assumptions.
Non-breaking fix that should apply ASAP and disruption is acceptable Unconditional skipWaiting() in install.

← Back to the Guides overview.