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 .
All major browsers provide DevTools panels for inspecting offline-first behavior. This guide covers Chrome DevTools (other browsers are similar).
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, ... }
Check How What to look for SW is registered Application > Service Workers Entry for your SW file with "activated" status SW is controlling the page Application > Service Workers "Clients: X tab(s) controlling" SW version is correct Application > Service Workers Source path matches current version (e.g., sw-v1.0.0.js) SW is running Application > Service Workers "Status: activated and is running" (not "stopped") Update flow works Application > Service Workers Click "Update on reload" → reload → check new version installs
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
Scenario How to Simulate New version available Update version.json on server, then reload the page SW waiting to activate Set AUTO_SKIP_WAITING = false in SW template, then deploy a new version SW activation In Application > Service Workers, click "skipWaiting" for the waiting SW, or call acceptUpdate() from the update prompt Unregister and reset In Application > Service Workers, click "Unregister". Also clear Cache Storage and IndexedDB if needed
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)
Strategy Cache Behavior Cache-First (default) After first fetch, response appears in cache and future requests skip the network Stale-While-Revalidate Cached 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
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" );
// 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" );
Three databases are relevant:
Database Object Store Purpose swoff-cache-tagstagsURL-to-tags index for cache invalidation swoff-queuemutationsPending mutation queue Your app's DB Your stores Application data (e.g., todos, users)
IndexedDB > swoff-queue > mutations
┌──────────────┬──────────┬──────────────────────┬──────────────────┐
│ id │ method │ url │ retryCount │
├──────────────┼──────────┼──────────────────────┼──────────────────┤
│ abc-123-def │ POST │ /api/todos │ 2 │
│ ghi-789-jkl │ PUT │ /api/todos/42 │ 0 │
└──────────────┴──────────┴──────────────────────┴──────────────────┘
Field What to Check Common Issues retryCountShould be 0 for fresh items High count means sync is failing methodPOST, PUT, DELETE Wrong method → server may reject urlFull API endpoint Wrong URL → 404 on sync timestampFIFO ordering Out-of-order processing → stale data tagsArray of tag strings Missing tags → cache not invalidated previousDataObject or null Missing on PUT/DELETE → rollback won't work
// 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 ();
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.
Feature How to Use Offline toggle Network tab > checkbox: "Offline". Simulates navigator.onLine = false Custom throttling Network tab > "Online" dropdown > Add custom profile for slow network simulation Filter by SW Filter bar: _isServiceWorkerIntercepted to see SW-handled requests only Inspect headers Click a request > Headers tab > Check X-SW-Cache-Strategy and X-SW-Cache-Tags are present on reads, absent on mutations Timing Click a request > Timing tab > "ServiceWorker" segment shows SW processing time
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
// 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;
}
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 }%)` );
}
});
}
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 │
└──────────────────────┘
Symptom Likely Cause Fix navigator.serviceWorker is undefinedHTTP (not HTTPS) or insecure context Serve over HTTPS or localhost Registration promise rejects with 404 Wrong SW file path Verify the generated SW path matches what you pass to register() Registration promise rejects with bad content type SW served without text/javascript Configure server to serve .js files with correct MIME type SW registers but never activates Scope mismatch or SW URL is out-of-scope Ensure SW is served from the root or set scope explicitly
Symptom Likely Cause Fix Old SW still running after deploy Browser hasn't checked for updates Call registration.update() or reload twice version.json fetch returns cached responseCDN caching or aggressive Cache-Control headers Set Cache-Control: no-cache on version.json Update prompt doesn't appear AUTO_SKIP_WAITING = true or update detection logic issueCheck sw-update-available event fires in console
Symptom Likely Cause Fix Old data shown after mutation Tags not attached to read request Verify fetchWithCache(url, { tags: [...] }) is called on reads invalidateByTag has no effectTag index is empty or SW message not received Check swoff-cache-tags in IndexedDB — verify entry exists for the tag Invalidation fails silently SW not controlling the page Navigate with SW-controlled reload (not hard refresh)
Symptom Likely Cause Fix Queue never drains navigator.onLine is false or isSyncing stuckCheck Network tab offline toggle, restart browser Queue items stuck at MAX_RETRIES Server returns non-2xx consistently Check server logs, verify request format and auth headers Rollback doesn't fire previousData not set on queued mutationAdd previousData when calling queueMutation() for PUT/DELETE
Symptom Likely Cause Fix Tab B doesn't update after Tab A mutation initCrossTabSync() not called in Tab BCall initCrossTabSync() after SW registration TAG_INVALIDATED message sent but not receivedSW not controlling Tab B Reload Tab B to re-establish SW control Cross-tab sync works inconsistently SW broadcasts to tabs from the same origin, but some tabs may have been loaded before SW installed Call 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 ());
}
}
// 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 );
Action Shortcut Open DevTools F12 or Ctrl+Shift+I / Cmd+Opt+IOpen Application panel Ctrl+Shift+9 / Cmd+Shift+9Open Network panel Ctrl+Shift+E / Cmd+Opt+EOpen Console Ctrl+Shift+J / Cmd+Opt+JNetwork offline toggle Network tab → checkbox Clear site data Application → Clear site data → Clear Force SW update Application → Service Workers → Update