This page gives the manual counterpart to the automated pipeline in Service Worker Error Telemetry & Debugging: what to do once a report tells you something is wrong.

The exact problem #

A user reports that the app shows an old version and an outdated offline page. You reload — everything is fine. You clear your cache — still fine. The bug lives in a state you do not have: an old worker still controlling their tab, a precache from a previous build, and an update that has been waiting since Tuesday.

Debugging service workers is mostly about reading state accurately before changing it. The most common mistake is clicking “Clear site data” first, which destroys the evidence and produces a working app that proves nothing.

The panel, in the order you should read it #

Open DevTools → Application → Service Workers and read top to bottom before touching any control.

What you see What it means What to do next
#4321 activated and is running Current worker is in control and awake Compare its script URL hash with your latest deploy
#4322 waiting to activate A new worker installed but cannot take over At least one tab is still controlled — this is the update-prompt case
installing that never completes The install handler is throwing or a precache request 404s Check the Console for the install error; verify the manifest
activated and is stopped Normal — the browser idles workers aggressively Nothing; it wakes on the next event
No worker listed Registration failed or was unregistered Check the registration call and its scope
redundant Replaced or failed The Console retains the reason from the failed install
Lifecycle states, symptoms and the controls that move between them Four state boxes in a row: installing, waiting, activating, activated. Arrows between them are labelled with the DevTools action or condition that causes the transition: install completes, skipWaiting or last tab closed, and activate completes. Under each state a note gives the symptom a user would report. installing precaching ok waiting old worker still controls skipWaiting activating old caches cleaned activated running or stopped symptom: update never arrives symptom: "still on the old version" symptom: brief cache gaps "update on reload" collapses installing → waiting → activated on every reload use it while iterating; turn it off before reproducing a user's stuck state

Diffing Cache Storage against the manifest #

Most “the offline page is wrong” reports are a cache/manifest mismatch. Run this in the page console, not the worker’s, so you can paste your manifest in easily.

// Paste in the page console: compares what is cached with what should be.
async function auditPrecache(expected /* array of URLs from your build manifest */) {
  const names = await caches.keys();
  const report = {};

  for (const name of names) {
    const cache = await caches.open(name);
    const keys = await cache.keys();
    const cached = new Set(keys.map((r) => new URL(r.url).pathname + new URL(r.url).search));
    report[name] = {
      size: cached.size,
      missing: expected.filter((u) => !cached.has(u)),
      orphaned: [...cached].filter((u) => !expected.includes(u)),
    };
  }

  console.table(
    Object.entries(report).map(([name, r]) => ({
      cache: name, entries: r.size, missing: r.missing.length, orphaned: r.orphaned.length,
    })),
  );
  return report;
}

Read the result this way: missing entries mean the install handler failed partway or the manifest changed after the precache; orphaned entries mean the activate cleanup did not run or filtered on a cache name pattern that stopped matching. Both point at the lifecycle handlers described in App Shell Precache & Offline Fallback Routing rather than at the fetch handler most people look at first.

A reproducible debugging loop #

  1. Reproduce the user’s state, do not fix it. If they are stuck on an old worker, use “Update on reload” off and unregister nothing until you have read the state.
  2. Check the script URL. The listed source URL and its query/hash tell you which build is in control. Half of all reports end here.
  3. Look for install errors in the Console. A single 404 in a precache list aborts the whole install, leaving a permanently installing worker and a user stuck on the previous build.
  4. Audit the caches. The script above, against the deployed manifest.
  5. Simulate offline in the Service Workers pane. This exercises the worker’s own fetch path; the Network panel’s offline toggle does not always cover the same branch.
  6. Change one thing. skipWaiting promotes the waiting worker; “Update” re-fetches the script; “Unregister” removes it entirely. Each answers a different question, and doing all three at once answers none.
// A safer alternative to "Clear site data" — clear caches only, keep IndexedDB.
async function clearCachesOnly() {
  const names = await caches.keys();
  await Promise.all(names.map((n) => caches.delete(n)));
  console.info(`cleared ${names.length} cache(s); IndexedDB untouched`);
}

That distinction matters when the bug is in recovery: IndexedDB is where the drafts and snapshots live, and clearing it deletes the very records you are debugging — the data described in Form State Recovery & Multi-Step Wizards.

What each DevTools action actually destroys Four rows. Update refetches the worker script and destroys nothing. SkipWaiting promotes the waiting worker and destroys nothing. Unregister removes the registration but leaves caches and IndexedDB. Clear site data removes caches, IndexedDB, registrations and everything else, and is marked as destroying the evidence. action what it destroys Update nothing — re-fetches the script and re-runs install skipWaiting nothing — promotes the waiting worker to active Unregister the registration only — caches and IndexedDB survive Clear site data caches + IndexedDB + registrations — the evidence too Reading the cache audit output Three result rows. A cache with missing entries points at a failed or partial install. A cache with orphaned entries points at an activate cleanup that no longer matches its naming pattern. A healthy cache has zero of both. cache missing / orphaned points at app-shell-v7 4 / 0 install aborted partway runtime-v6 0 / 118 activate cleanup pattern drifted app-shell-v8 0 / 0 healthy

Verification #

  1. Prove you can reproduce the stuck state. With “Update on reload” off, deploy a change and confirm you see waiting to activate — that is the state your users are in.
  2. Break a precache entry. Add a nonexistent URL to the manifest; the worker must stay installing and the Console must name the failing request. This teaches you the signature.
  3. Run the cache audit. Confirm zero missing and zero orphaned entries on a healthy build; anything else is a real finding.
  4. Test offline for real. Check the offline box, navigate to an uncached route, and confirm the offline fallback renders — the behaviour covered in Serving an Offline Fallback Page from the Service Worker.
  5. Confirm your clean-up is surgical. After clearCachesOnly(), an IndexedDB draft written before the clear must still be there.

One habit worth adopting: before changing anything, copy the worker’s script URL, the cache key list and the registration state into the incident notes. Those three values are what let you compare a user’s broken state with a healthy one later, and every DevTools action you take from that point destroys some of them.

Frequently asked questions #

Why does my new service worker stay stuck in ‘waiting’?

Because a client is still controlled by the old worker; waiting is by design. Click skipWaiting while debugging and ship a real update prompt for users.

Why does the offline checkbox behave differently from network throttling?

The Service Workers pane’s checkbox exercises the worker’s own request path; the Network panel’s offline mode affects page requests, and they do not always cover the same branch.

Is ‘Clear site data’ safe while debugging a recovery bug?

It deletes exactly what you are inspecting. Copy the relevant records first, then clear specific buckets.