Skip to content

Updating Service Workers

Deploying a new version of your PWA means deploying a new service worker. Understanding how the browser detects and applies that update — and how to communicate it to users — prevents subtle bugs and stale-asset surprises.

The browser checks your service worker file for changes on every page load (and whenever you call registration.update() manually). It compares the new download byte-for-byte against the currently installed SW. Even a single byte difference — a changed cache version string, a new comment, a whitespace tweak — triggers the update flow:

  1. The new SW is downloaded and parsed.
  2. The new SW enters the install phase.
  3. If installation succeeds, the new SW sits in Waiting — it cannot activate while the old SW still controls open tabs.

The most common source of confusion in production PWAs:

  • A user opens your app. The old SW activates and controls the tab.
  • You deploy v2. The browser installs the new SW, but it sits in Waiting.
  • The user keeps the tab open all day.
  • The new SW never activates — the user never sees v2.

Calling self.skipWaiting() inside the new SW’s install handler forces it to activate immediately, bypassing the wait:

self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('v2').then((cache) => cache.addAll(['/', '/app.js', '/styles.css']))
.then(() => self.skipWaiting())
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim());
});

The danger: the old page is still running. If v2 has a different cache schema or asset URLs, the already-loaded page may request resources the new SW does not serve correctly. The result is a broken mid-session experience.

Recommendation: use skipWaiting() only when your asset URLs are versioned (e.g., app.abc123.js) so old and new assets are compatible, or always prompt the user to reload.

The safest pattern: detect the waiting worker from the page and show a non-intrusive banner.

// main.js — page code
async function registerSw() {
if (!('serviceWorker' in navigator)) return;
const registration = await navigator.serviceWorker.register('/sw.js');
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
if (!newWorker) return;
newWorker.addEventListener('statechange', () => {
if (
newWorker.state === 'installed' &&
navigator.serviceWorker.controller
) {
// A new SW is waiting — show update prompt
showUpdateBanner(registration);
}
});
});
}
function showUpdateBanner(registration) {
const banner = document.createElement('div');
banner.textContent = 'A new version is available. ';
const btn = document.createElement('button');
btn.textContent = 'Reload';
btn.onclick = () => {
if (registration.waiting) {
// Tell the waiting SW to skip waiting, then reload
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
}
window.location.reload();
};
banner.appendChild(btn);
document.body.prepend(banner);
}
registerSw();

The corresponding SW listens for that message:

// sw.js — service worker
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});

This keeps skipWaiting() entirely user-driven — the new version only activates when the user explicitly clicks Reload, preventing mid-session breakage.

registration.update() — proactive checking

Section titled “registration.update() — proactive checking”

By default the browser checks for SW updates on navigation. For single-page apps where the user may never trigger a navigation, call registration.update() on an interval:

setInterval(() => registration.update(), 60 * 60 * 1000); // check every hour
StepWhy
Change at least one byte in sw.jsTriggers the browser’s byte-diff check
Version your cache names (v2, v3)Allows the activate handler to delete stale caches
Delete old caches in activateAvoids storing stale assets indefinitely
Prompt users to reload instead of force-skipWaiting()Prevents mid-session asset mismatches
Call clients.claim() after skipWaiting()Takes control of open pages immediately
How does the browser determine that a service worker file has changed?
Why does a newly installed service worker sit in Waiting?
What is the risk of calling self.skipWaiting() unconditionally in the install handler?
Which approach keeps skipWaiting() user-driven and avoids mid-session breakage?