Swoff Swoff

Debugging

Debug service workers, cache storage, IndexedDB, and network behavior using browser DevTools.

Prerequisites: You have the core patterns set up — SW Template, Client Registration, and API Integration.

Overview

All major browsers provide DevTools panels for inspecting offline-first behavior. This guide covers Chrome DevTools (other browsers are similar).

Service Workers

Application > Service Workers

This panel shows all registered service workers for the current origin.

Panel Layout (Chrome DevTools):
  Application
    ├── Service Workers
    │     ├── Registration: swoff/sw-v1.0.0.js (scope: /)
    │     ├── Status: activated and is running
    │     ├── Clients: 1 tab controlling
    │     └── Update on reload: ☑
    ├── Cache Storage
    │     ├── swoff-runtime
    │     │     ├── /api/todos  (tagged: todos)
    │     │     ├── /api/users  (tagged: users)
    │     │     └── ...
    │     └── sw-v1.0.0 (install cache)
    │           ├── /
    │           ├── /index.html
    │           ├── /assets/index-*.js
    │           └── ...
    └── IndexedDB
          ├── swoff-cache-tags
          │     └── tags
          │           ├── { url: "/api/todos", tags: ["todos"] }
          │           └── { url: "/api/users", tags: ["users"] }
          └── swoff-queue
                └── mutations
                      └── { id, method, url, retryCount, ... }

Key Checks

CheckHowWhat to look for
SW is registeredApplication > Service WorkersEntry for your SW file with "activated" status
SW is controlling the pageApplication > Service Workers"Clients: X tab(s) controlling"
SW version is correctApplication > Service WorkersSource path matches current version (e.g., sw-v1.0.0.js)
SW is runningApplication > Service Workers"Status: activated and is running" (not "stopped")
Update flow worksApplication > Service WorkersClick "Update on reload" → reload → check new version installs

Debugging SW Messages

The navigator.serviceWorker.controller.postMessage() calls in invalidatedByTag and the SW's postMessage() calls in invalidateByTag can be logged:

// In your app, listen for all SW messages:
navigator.serviceWorker.addEventListener("message", (event) => {
  console.log("[SW → Client]", event.data);
});
// In the SW, log all received messages:
self.addEventListener("message", (event) => {
  console.log("[Client → SW]", event.data);
  // ... existing handlers ...
});

Filter console output by source:

  • Client messages: Filter by [Client → SW] prefix
  • SW messages: Filter by [SW → Client] prefix
  • Or select "Service Worker" from the console's context dropdown

Forcing Update Scenarios

ScenarioHow to Simulate
New version availableUpdate version.json on server, then reload the page
SW waiting to activateSet AUTO_SKIP_WAITING = false in SW template, then deploy a new version
SW activationIn Application > Service Workers, click "skipWaiting" for the waiting SW, or call acceptUpdate() from the update prompt
Unregister and resetIn Application > Service Workers, click "Unregister". Also clear Cache Storage and IndexedDB if needed

Cache Storage

Application > Cache Storage

Inspect the swoff-runtime cache to verify cached responses:

Cache Storage > swoff-runtime
  ├── /api/todos                   → Response (JSON)
  ├── /api/todos/42                → Response (JSON)
  └── /api/users/me                → Response (JSON)

Verifying Cache Strategies

StrategyCache Behavior
Cache-First (default)After first fetch, response appears in cache and future requests skip the network
Stale-While-RevalidateCached response is served immediately, then a background fetch updates the cached entry
Mutation (bypass)No caching — POST/PUT/DELETE requests should never appear in cache

Manual Testing

Test cache invalidation manually:

// In DevTools console:
async function debugInvalidation(tag) {
  const cache = await caches.open("swoff-runtime");
  const keys = await cache.keys();
  console.log("Cache entries before invalidation:", keys.map(r => r.url));

  invalidateByTag(tag);

  setTimeout(async () => {
    const keysAfter = await cache.keys();
    console.log("Cache entries after invalidation:", keysAfter.map(r => r.url));
  }, 100);
}

debugInvalidation("todos");

Force Cache Repopulation

// Delete a specific cached entry and refetch:
async function refetchUrl(url) {
  const cache = await caches.open("swoff-runtime");
  await cache.delete(url);
  const fresh = await fetch(url, {
    headers: { "X-SW-Cache-Strategy": "read" },
  });
  console.log("Refetched:", await fresh.clone().json());
}

refetchUrl("/api/todos");

IndexedDB

Application > IndexedDB

Three databases are relevant:

DatabaseObject StorePurpose
swoff-cache-tagstagsURL-to-tags index for cache invalidation
swoff-queuemutationsPending mutation queue
Your app's DBYour storesApplication data (e.g., todos, users)

Verifying Queue State

IndexedDB > swoff-queue > mutations
  ┌──────────────┬──────────┬──────────────────────┬──────────────────┐
  │ id           │ method   │ url                  │ retryCount       │
  ├──────────────┼──────────┼──────────────────────┼──────────────────┤
  │ abc-123-def  │ POST     │ /api/todos           │ 2                │
  │ ghi-789-jkl  │ PUT      │ /api/todos/42        │ 0                │
  └──────────────┴──────────┴──────────────────────┴──────────────────┘
FieldWhat to CheckCommon Issues
retryCountShould be 0 for fresh itemsHigh count means sync is failing
methodPOST, PUT, DELETEWrong method → server may reject
urlFull API endpointWrong URL → 404 on sync
timestampFIFO orderingOut-of-order processing → stale data
tagsArray of tag stringsMissing tags → cache not invalidated
previousDataObject or nullMissing on PUT/DELETE → rollback won't work

Manual Queue Manipulation

// Skip a stuck mutation by deleting it directly:
const db = await new Promise((resolve, reject) => {
  const req = indexedDB.open("swoff-queue");
  req.onsuccess = () => resolve(req.result);
  req.onerror = () => reject(req.error);
});
const tx = db.transaction("mutations", "readwrite");
tx.objectStore("mutations").delete("abc-123-def");

// Clear all pending mutations (dev only):
tx.objectStore("mutations").clear();

Verifying Tag Index

IndexedDB > swoff-cache-tags > tags
  ┌─────────────────────┬─────────────────────┐
  │ url                 │ tags                │
  ├─────────────────────┼─────────────────────┤
  │ /api/todos          │ ["todos"]           │
  │ /api/todos/42       │ ["todos", "todo:42"]│
  │ /api/users/me       │ ["users"]           │
  └─────────────────────┴─────────────────────┘

If a mutation's tags don't match any entry here, invalidation will have no visible effect. Verify tag naming consistency between reads and writes.

Network

Network Tab

FeatureHow to Use
Offline toggleNetwork tab > checkbox: "Offline". Simulates navigator.onLine = false
Custom throttlingNetwork tab > "Online" dropdown > Add custom profile for slow network simulation
Filter by SWFilter bar: _isServiceWorkerIntercepted to see SW-handled requests only
Inspect headersClick a request > Headers tab > Check X-SW-Cache-Strategy and X-SW-Cache-Tags are present on reads, absent on mutations
TimingClick a request > Timing tab > "ServiceWorker" segment shows SW processing time

Verifying Headers

Request headers for GET /api/todos:
  X-SW-Cache-Strategy: read          ← present for reads
  X-SW-Cache-Tags: todos             ← present when tags option passed
  X-SW-Stale: true                   ← present when staleWhileRevalidate: true

Request headers for POST /api/todos:
  X-SW-Cache-Strategy: mutation      ← present for mutations (or absent, defaults to mutation)
  X-SW-Cache-Tags: (not sent)        ← absent for mutations

Tracking Cache Strategy

// Log a marker in the Network tab for each fetch strategy:
export async function fetchWithCache(input, options = {}) {
  // ... existing code ...

  const response = await fetch(input, { ...options, headers });

  // Clone once to inspect
  const clone = response.clone();

  if (options.staleWhileRevalidate) {
    console.log(`[Cache Strategy] stale-while-revalidate for ${url}`);
  } else {
    console.log(`[Cache Strategy] cache-first for ${url}`);
  }

  return response;
}

PostMessage Communication

The message flow between client and SW is critical for invalidation and cross-tab sync. Monitor it:

// App.js — log all SW → client messages
if (navigator.serviceWorker) {
  navigator.serviceWorker.addEventListener("message", (event) => {
    const { type, tag } = event.data;
    if (type === "TAG_INVALIDATED") {
      console.log(`[Cross-Tab Sync] Cache invalidated for tag: "${tag}"`);
    }
    if (type === "SW_PROGRESS") {
      const { percent, downloaded, total } = event.data;
      console.log(`[SW Install] ${downloaded}/${total} (${percent}%)`);
    }
  });
}

Message Flow Diagram

addTodo() → invalidateByTag("todos")
    ↓                        ↑
postMessage(INVALIDATE_TAG)  dispatchEvent("cache-invalidated")
    ↓                        ↑
 ┌──────────────────────┐    │
 │ Service Worker       │    │
 │                      │    │
 │ 1. findByTag("todos")│    │
 │ 2. delete from cache │    │
 │ 3. post to ALL tabs  │────┘
 │    TAG_INVALIDATED   │
 └──────────────────────┘

Troubleshooting

SW Not Registering

SymptomLikely CauseFix
navigator.serviceWorker is undefinedHTTP (not HTTPS) or insecure contextServe over HTTPS or localhost
Registration promise rejects with 404Wrong SW file pathVerify the generated SW path matches what you pass to register()
Registration promise rejects with bad content typeSW served without text/javascriptConfigure server to serve .js files with correct MIME type
SW registers but never activatesScope mismatch or SW URL is out-of-scopeEnsure SW is served from the root or set scope explicitly

SW Not Updating

SymptomLikely CauseFix
Old SW still running after deployBrowser hasn't checked for updatesCall registration.update() or reload twice
version.json fetch returns cached responseCDN caching or aggressive Cache-Control headersSet Cache-Control: no-cache on version.json
Update prompt doesn't appearAUTO_SKIP_WAITING = true or update detection logic issueCheck sw-update-available event fires in console

Cache Not Invalidating

SymptomLikely CauseFix
Old data shown after mutationTags not attached to read requestVerify fetchWithCache(url, { tags: [...] }) is called on reads
invalidateByTag has no effectTag index is empty or SW message not receivedCheck swoff-cache-tags in IndexedDB — verify entry exists for the tag
Invalidation fails silentlySW not controlling the pageNavigate with SW-controlled reload (not hard refresh)

Mutation Queue Not Processing

SymptomLikely CauseFix
Queue never drainsnavigator.onLine is false or isSyncing stuckCheck Network tab offline toggle, restart browser
Queue items stuck at MAX_RETRIESServer returns non-2xx consistentlyCheck server logs, verify request format and auth headers
Rollback doesn't firepreviousData not set on queued mutationAdd previousData when calling queueMutation() for PUT/DELETE

Cross-Tab Sync Not Working

SymptomLikely CauseFix
Tab B doesn't update after Tab A mutationinitCrossTabSync() not called in Tab BCall initCrossTabSync() after SW registration
TAG_INVALIDATED message sent but not receivedSW not controlling Tab BReload Tab B to re-establish SW control
Cross-tab sync works inconsistentlySW broadcasts to tabs from the same origin, but some tabs may have been loaded before SW installedCall initCrossTabSync() with a retry: re-register listener if SW becomes available later
// Robust cross-tab sync initialization:
function initCrossTabSync() {
  function setup() {
    if (!navigator.serviceWorker?.controller) return false;
    navigator.serviceWorker.addEventListener("message", (event) => {
      if (event.data.type === "TAG_INVALIDATED" && event.data.tag) {
        window.dispatchEvent(
          new CustomEvent("cache-invalidated", {
            detail: { tags: [event.data.tag] },
          }),
        );
      }
    });
    return true;
  }

  if (!setup()) {
    navigator.serviceWorker?.ready.then(() => setup());
  }
}

Quick Reference

Console Snippets

// Check SW registration
const reg = await navigator.serviceWorker.ready;
console.log("SW scope:", reg.scope, "| Active:", !!reg.active);

// List cache entries
const cache = await caches.open("swoff-runtime");
const keys = await cache.keys();
console.log("Cached URLs:", keys.map(r => r.url));

// Count pending mutations
const db = await new Promise((r, rej) => {
  const req = indexedDB.open("swoff-queue");
  req.onsuccess = () => r(req.result);
  req.onerror = () => rej(req.error);
});
const count = await new Promise((r, rej) => {
  const req = db.transaction("mutations").objectStore("mutations").count();
  req.onsuccess = () => r(req.result);
  req.onerror = () => rej(req.error);
});
console.log("Pending mutations:", count);

// Manually trigger a sync
window.dispatchEvent(new Event("online"));

// Check cross-tab sync setup
console.log("Cross-tab sync active:",
  navigator.serviceWorker?.onmessage !== null);

DevTools Shortcuts

ActionShortcut
Open DevToolsF12 or Ctrl+Shift+I / Cmd+Opt+I
Open Application panelCtrl+Shift+9 / Cmd+Shift+9
Open Network panelCtrl+Shift+E / Cmd+Opt+E
Open ConsoleCtrl+Shift+J / Cmd+Opt+J
Network offline toggleNetwork tab → checkbox
Clear site dataApplication → Clear site data → Clear
Force SW updateApplication → Service Workers → Update

Next Steps

On this page