# Debugging service workers

> Where Chrome DevTools, Firefox DevTools, and Safari Web Inspector expose a service worker's state, how to force an update, bypass the worker, simulate offline, inspect Cache Storage, and read worker console output, with the exact panel labels.

Debugging a service worker means reading three things the page cannot show on its own: the registration's state (installing, waiting, active), the contents of Cache Storage, and which requests the worker answered. Chrome 40+ and Edge 79+ expose all three in DevTools › Application; Firefox 44+ splits them between `about:debugging` and the Storage panel; Safari 11.1+ exposes registrations in Web Inspector but offers no cache viewer or forced-update control (BCD `api.ServiceWorker`; vendor docs in the sources).

## How it works

The panels differ, but each one is a view onto the same registration object and the same `caches` the worker sees, so anything a panel does can also be done from the worker's console context.

### Chrome and Edge DevTools

Application › **Service workers** lists every registration for the origin with its scope, script URL, and a status line of the form `#<id> activated and is running`, `#<id> waiting to activate`, or `#<id> trying to install`. Three checkboxes at the top change behaviour for the current tab only: **Offline** makes `fetch()` inside the worker and from the page fail as if the network were down, **Update on reload** forces a byte-comparison check and installs the fetched script as a new worker on every navigation (bypassing the 24-hour script cache rule), and **Bypass for network** passes all requests straight to the network without firing `fetch` in the worker. Each registration has **Update**, **Unregister**, and, for a worker in the waiting state, a **skipWaiting** link that calls `skipWaiting()` on it without a code change. Application › **Cache storage** lists caches as `<cache name> - <origin>` and shows each entry's request URL, response headers, and a body preview, with per-entry and per-cache delete. In the Network panel, the **Size** column reads `(ServiceWorker)` for responses the worker produced, and requests the worker itself issued carry a gear icon. The console's context selector (top-left of the Console panel, default `top`) lists the worker by its script URL; selecting it routes `console.log` from the worker and lets you evaluate `await caches.keys()` or `await self.clients.matchAll()` in the worker's global scope.

### Firefox DevTools

`about:debugging#/runtime/this-firefox` lists registrations under **Service Workers** with scope, state, and two buttons: **Inspect**, which opens a toolbox whose Console is the worker's context, and **Unregister**. The toolbox for a page also shows the worker under Application › Service Workers (Firefox 79+), with a **Start** button for a stopped worker. Firefox has no "update on reload" switch; the equivalent is `registration.update()` from the page console, or **Unregister** followed by a reload. Cache Storage appears in the Storage panel under **Cache Storage**, listing caches and their request URLs and letting you delete entries.

### Safari Web Inspector

Enable the Develop menu (Safari › Settings › Advanced › **Show features for web developers**, labelled **Show Develop menu in menu bar** before Safari 17) and open Develop › **Service Workers**, which lists registrations by scope and opens a Web Inspector window attached to the worker's context. The Storage tab shows IndexedDB and Local Storage but not Cache Storage, so `await caches.keys()` and `await (await caches.open(name)).keys()` in the worker console are the way to list cached entries. There is no offline simulation or forced update; use Network Link Conditioner (macOS) or airplane mode (iOS) and `registration.update()`.

### Reading the symptoms

The failure modes below each have a visible signature in one of the panels.

| Symptom | Where it shows | Cause and fix |
|---|---|---|
| New code not running | Chrome: `waiting to activate` beside a second worker | Old tabs are still controlled; close them, click **skipWaiting**, or ship a `skipWaiting()` path |
| Deploy ignored for up to a day | Network panel shows the script served `(disk cache)` | HTTP caching of the script; register with `updateViaCache: 'none'` or send `Cache-Control: no-cache` on the script URL |
| Requests not intercepted | Network **Size** column lacks `(ServiceWorker)` | The page is outside the scope, the worker is not yet active, or **Bypass for network** is on |
| Cache empty after install | Cache storage shows no cache, worker console shows a rejected `addAll()` | A precache URL returned a non-2xx status; `addAll()` is atomic and stores nothing on any failure |
| Install hangs | Status stuck at `trying to install` | `event.waitUntil()` received a promise that does not settle |

:::observed
In Chrome DevTools (English UI), Application › Service workers shows a freshly deployed second version as `#<id> waiting to activate` directly under the running one, and the **skipWaiting** link beside it activates the new worker without reloading; the same page in Firefox shows the two versions only after **Inspect** in `about:debugging`, and Safari's Develop › Service Workers lists one entry per scope with no state text. The panel labels are those in the Chrome DevTools "Debug Progressive Web Apps" guide and the Firefox `about:debugging` documentation cited in the sources.
:::

## Examples

The two snippets reproduce the two most-used panel actions from code, so they work in browsers without the corresponding button.

### Forcing an update check and listing cache contents from the page console

`registration.update()` performs the same byte-comparison check that "Update on reload" triggers; `caches.keys()` from the page lists the same caches the Application panel shows, because `caches` is shared between page and worker on one origin.

```js
const registration = await navigator.serviceWorker.getRegistration();
await registration.update();                 // fetches /sw.js, installs if it differs
console.log(registration.waiting?.state);    // "installed" when an update is waiting

for (const name of await caches.keys()) {
  const cache = await caches.open(name);
  const keys = await cache.keys();
  console.log(name, keys.map((request) => request.url));
}
```

The page-side listing works in Safari too, which has no cache viewer; the cost is that opaque responses show only their URL, since an opaque body is unreadable in every context.

### A debug flag that logs every fetch decision, with a no-op in production

Logging inside the `fetch` handler is the manual equivalent of Workbox's development-build logs. Keying it on the registration URL keeps the production worker silent without a build step; when `console` is unavailable (older WebView builds) the guard falls back to doing nothing.

```js
// sw.js
const DEBUG = new URL(self.location.href).searchParams.has('debug'); // register('/sw.js?debug')

self.addEventListener('fetch', (event) => {
  event.respondWith((async () => {
    const cached = await caches.match(event.request);
    if (DEBUG && typeof console !== 'undefined') {
      console.log(cached ? 'cache' : 'network', event.request.method, event.request.url);
    }
    return cached ?? fetch(event.request);
  })());
});
```

Registering `/sw.js?debug` installs a different script URL and therefore a separate worker; remember to re-register without the query string before measuring, otherwise the logging worker stays in control of that tab.

## See also

- [Debug Progressive Web Apps](https://developer.chrome.com/docs/devtools/progressive-web-apps/) (developer.chrome.com)
- [about:debugging](https://firefox-source-docs.mozilla.org/devtools-user/about_colon_debugging/index.html) (firefox-source-docs.mozilla.org)
- [skipWaiting() and the update flow](/reference/service-worker/update-skipwaiting/)
- [Service worker lifecycle](/reference/service-worker/lifecycle/)
- [Cache API](/reference/service-worker/cache-api/)
- [Service worker registration and scope](/reference/service-worker/registration-scope/)