⏻
HELGE SVERREAll-stack Developer
Bergen, Norway • v13.0
est. 2012  |  197 repos  |  12.8k+ contributions
Tools  |   Theme:
Removing a Stale Service Worker from localhost
November 4, 2026

The problem

You open http://localhost:3000 and get a different project, one you built weeks ago, even though your current dev server is running and the old one isn't.

A service worker is registered per origin, and localhost:3000 is the same origin for every project that has ever run on that port. If one of them was a PWA, its worker is still installed in your browser. Workbox's navigateFallback answers every page load with the cached index.html without touching the network, so requests never reach your dev server.

Typical source: building a PWA (for example with vite-plugin-pwa) and serving the production build on a common port like 3000 or 8080.

Find every registered worker

Chrome, Edge, Arc, Brave: open chrome://serviceworker-internals. It lists every registration in the profile with its scope and script URL. Search the page for localhost.

Firefox: open about:serviceworkers.

From the shell (Chrome, read-only): the cached worker scripts live in the profile folder, so you can tell which project a worker belongs to:

cd ~/Library/Application\ Support/Google/Chrome/Default/Service\ Worker   # or "Profile 1", etc.
rg -a -l 'localhost:3000' ScriptCache
strings -a ScriptCache/<file> | rg -o 'https?://localhost:3000[^ "]*'

A hashed file name such as workbox-2fbc6a65.js can be matched against the dist/ folders of your projects. Don't delete files in this folder while the browser is running, since that can corrupt the profile.

Remove it

Pick one:

  1. Unregister: on chrome://serviceworker-internals, find the entry with scope http://localhost:3000/ and click Unregister.

  2. Clear everything for the origin: open http://localhost:3000, then DevTools → Application → Storage → Clear site data. This also removes the precache in Cache Storage.

  3. Console, on the affected origin:

    (await navigator.serviceWorker.getRegistrations()).forEach((r) => r.unregister());
    (await caches.keys()).forEach((k) => caches.delete(k));
    

Hard-reload afterwards. In Safari, remove localhost under Settings → Privacy → Manage Website Data.

Remove it for everyone (kill switch)

If other people or other browsers have the stale worker, a page can't unregister it for them, because the old worker intercepts the page. The browser does still fetch the worker script from the network when it checks for updates. Serve this at the same path as the old script (for example /sw.js) and it replaces the old worker, then removes itself:

// sw.js: replaces a stale worker, deletes its caches and unregisters.
self.addEventListener("install", () => self.skipWaiting());

self.addEventListener("activate", (event) => {
  event.waitUntil(
    (async () => {
      const keys = await caches.keys();
      await Promise.all(keys.map((key) => caches.delete(key)));
      await self.registration.unregister();
      const windows = await self.clients.matchAll({ type: "window" });
      windows.forEach((client) => client.navigate(client.url));
    })(),
  );
});

The script URL has to match the registered one exactly. You can read it from chrome://serviceworker-internals.




<!-- generated with nested tables and zero regrets -->