Skip to content

Concept in Service Worker

Removing a service worker safely (kill switch)

Published

Widely available since 2018-04W3C draft

A service worker that answers requests from a cache keeps answering them after the server is fixed, because the worker, not the server, decides what a controlled page receives. Removing the registration is the repair, and ServiceWorkerRegistration.unregister() is the call that performs it.

The removal is two separate things that are easy to confuse: dropping the registration, and releasing the pages the old worker still controls. The specification separates them explicitly, and the second one is not under the page’s control.

unregister() takes no arguments and returns a promise. The promise resolves to false when no registration was found, and otherwise to true “irrespective of whether unregistration happened or not (it may not unregister if someone else just called ServiceWorkerContainer.register() with the same scope.)”; the service worker finishes any ongoing operations before it is unregistered (ServiceWorkerRegistration: unregister() method, developer.mozilla.org). MDN records the method as Baseline widely available, with support across browsers since April 2018, and as available only in secure contexts in some or all supporting browsers.

The specification’s Unregister algorithm shows why the resolved value says so little about the outcome. It gets the registration for the job’s storage key and scope url; if that is null it resolves the job promise with false and finishes. Otherwise it removes the registration map entry for that key and scope, resolves the job promise with true, and invokes Try Clear Registration (Service Workers, w3c.github.io). So true reports that the map entry was removed, not that the worker has stopped.

The clearing is deferred. The specification notes that when Try Clear Registration does not trigger Clear Registration at that point, “Clear Registration is tried again when the last client using the registration is unloaded or the extend lifetime promises for the registration’s service workers settle”. The note on the method itself is blunter: the registration containing the controlled client’s active service worker “is effective until all the service worker clients (including itself) using this service worker registration unload. That is, the unregister() method only affects subsequent navigations.”

That is the behaviour that makes a kill switch feel broken. The tab that ran the call still has its old controller, so its next fetch still reaches the old handler. A reload, or closing the tab, is what ends the registration’s effect.

The Unregister algorithm operates on the registration map and the registration’s workers. Cached responses are reached through a different interface: CacheStorage.keys() resolves with an array of strings naming every Cache object the CacheStorage tracks, in the order they were created (CacheStorage: keys() method, developer.mozilla.org), and CacheStorage.delete() finds the Cache matching the cacheName, deletes it and resolves to true, or resolves to false when no such cache is found (CacheStorage: delete() method, developer.mozilla.org). Cleanup can delete an explicit list of known cache names directly; keys() is needed when those names must first be discovered.

What keys() returns is every cache name in that CacheStorage object’s relevant name-to-cache map, not a list attributed to the removed worker. The caches to delete are the ones the application being removed owns, which is why the examples below match an application-specific prefix instead of the full list.

The first example removes registrations from a page that still loads; the second covers the case where the broken worker is the only thing the browser will run.

Unregistering every registration for the current client’s storage key

Section titled “Unregistering every registration for the current client’s storage key”

navigator.serviceWorker.getRegistrations() gets all the ServiceWorkerRegistration objects associated with a ServiceWorkerContainer, in an array (ServiceWorkerContainer: getRegistrations() method, developer.mozilla.org), which is what lets one call cover a worker registered at a scope the current page does not know about.

// Every cache this application creates is named with this prefix, so only names
// carrying it may be deleted here.
const OWNED_CACHE_PREFIX = 'app-';
async function removeAllWorkers() {
if (!('serviceWorker' in navigator)) {
// No worker API in this context: there is nothing registered to remove.
return { supported: false, removed: 0 };
}
const registrations = await navigator.serviceWorker.getRegistrations();
const results = await Promise.all(registrations.map((reg) => reg.unregister()));
const names = await caches.keys();
const owned = names.filter((name) => name.startsWith(OWNED_CACHE_PREFIX));
await Promise.all(owned.map((name) => caches.delete(name)));
return { supported: true, removed: results.filter(Boolean).length, cachesDeleted: owned.length };
}

The unsupported branch returns a structured result because this context does not expose the Service Worker API. Pages already controlled by the old worker keep it until they unload, so the caller still has to reload them. The prefix filter is what keeps the cleanup to this application; substitute the naming scheme the removed worker actually used, and keep an explicit list of names instead if the scheme has no common prefix.

Deploying a worker that unregisters itself

Section titled “Deploying a worker that unregisters itself”

When the deployed worker serves a stale copy of the page that would run the previous example, the replacement has to arrive as a service worker. self.registration returns a reference to the worker’s own ServiceWorkerRegistration object (ServiceWorkerGlobalScope: registration property, developer.mozilla.org), and skipWaiting() lets the new worker activate without waiting for the old one’s clients to close (ServiceWorkerGlobalScope: skipWaiting() method, developer.mozilla.org).

// sw.js, deployed in place of the worker being removed
const OWNED_CACHE_PREFIX = 'app-';
self.addEventListener('install', () => {
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil((async () => {
const names = await caches.keys();
await Promise.all(
names
.filter((name) => name.startsWith(OWNED_CACHE_PREFIX))
.map((name) => caches.delete(name)),
);
await self.registration.unregister();
})());
});

This script registers no fetch handler, so once it is the active worker its clients go to the network on their next navigation. The registration itself is removed by the unregister() call, and the clearing of the registration’s workers happens when the last client using it unloads, per the specification note above.

Specifications

SpecificationStatus
Service WorkersW3C draft
  • Legend
  • Yes
  • Partial
  • Flag
  • No
  • Unknown
Browser / PlatformSupportVersionsConfidenceSourceNotes
Chrome (Android)Yes40mediumsource—
Chrome (Desktop)Yes40mediumsource—
Edge (Desktop)Yes17mediumsource—
Safari (iOS)Yes11.3mediumsource1
Safari (macOS)Yes11.1mediumsource—
Firefox (Desktop)Yes44mediumsource—
Samsung InternetYes4.0mediumsource—
  1. Storage may be evicted after prolonged non-use.

Source data: /compatibility/service-worker.json · Global usage: 93 % (StatCounter 2026-05)

Source: spec · MDN · Last verified 2026-06-24 · Confidence: medium (computed from sources)