How to Invalidate Service Worker Caches on Deploy
This guide covers the cache layer that purges cannot reach, within Cache Invalidation Patterns in Advanced Caching Strategies & CDN Architecture. A service worker's Cache Storage is controlled entirely by your service worker code. It ignores Cache-Control, cannot be purged by the CDN, and persists across visits. If the service worker serves the app shell from cache, a deploy does nothing for returning users until the service worker itself updates — and a careless update can delete caches still in use by open tabs, causing broken pages.
Done well, service worker caching makes repeat visits nearly instant and keeps the site usable offline, while deploys roll out promptly. The mechanisms are cache versioning (each release writes to new cache names), cleanup on activation (old caches are deleted only when the new worker takes control), and an update flow that decides when open tabs switch to the new version.
Rapid Diagnosis
- Open DevTools → Application → Service Workers. Check whether a new worker is "waiting to activate" after a deploy.
- Inspect Cache Storage. Look for cache names without versions (
static,pages) that never change, or many accumulated old versions. - Check what is cached. If HTML is served cache-first from the service worker, users see old releases until the worker updates.
- Check the service worker script's HTTP caching. The
sw.jsfile itself should be served withCache-Control: no-cache(browsers also cap its HTTP cache lifetime) so updates are detected.
Root Cause Analysis
1. Unversioned cache names. New assets are added to the same cache as old ones; old assets accumulate and stale HTML persists.
2. Cache-first HTML. The shell is served from cache indefinitely; the network version is never used while the old worker controls the page.
3. Workers stuck waiting. New workers wait until all tabs controlled by the old one close; long-lived tabs keep old versions alive for days.
4. Over-eager cleanup. Deleting old caches during install (instead of activate) breaks tabs still running the old version that request lazily loaded chunks.
Step-by-Step Resolution
1. Version cache names with the release
// sw.js — BUILD_ID is injected at build time.
const VERSION = BUILD_ID;
const PRECACHE = `precache-${VERSION}`;
self.addEventListener('install', (event) => {
event.waitUntil(caches.open(PRECACHE).then((c) => c.addAll(PRECACHE_URLS)));
});
// trade-off: every release downloads the full precache list. Keep it to the
// shell and critical assets; let other assets be cached at runtime.
2. Delete old caches only on activate
self.addEventListener('activate', (event) => {
event.waitUntil((async () => {
const keep = new Set([PRECACHE, `runtime-${VERSION}`]);
for (const name of await caches.keys()) if (!keep.has(name)) await caches.delete(name);
await self.clients.claim();
})());
});
// trade-off: claim() makes the new worker control open pages immediately;
// those pages still run old JavaScript, which may now fetch through a worker
// that no longer has old chunks cached. Keep old hashed assets on the server.
Expected outcome: caches never mix releases, and nothing is deleted while the old worker is still in charge.
3. Serve HTML network-first (or stale-while-revalidate)
Navigations should try the network first (with a timeout and cache fallback for offline), so a deploy is visible on the next page load even before the new worker activates. Workbox's NetworkFirst with networkTimeoutSeconds is a ready-made implementation.
4. Decide the update UX
Either prompt users ("A new version is available — refresh") and call skipWaiting on confirmation, or skip waiting automatically and reload at a safe moment. Handling service worker updates with skipWaiting compares the approaches.
Verification
Deploy to staging with a tab open. Confirm the new worker installs, waits (or activates per your policy), and that after activation Cache Storage contains only the new version's caches. Load a page in a new tab and confirm it shows the new release. Check that the old tab can still navigate without chunk errors.
Worked Example: A PWA Stuck on Old Releases
A progressive web app served its shell cache-first from an unversioned app-shell cache and never deleted old entries. Users who installed it saw releases weeks late, and storage usage grew past 300MB on some devices. The team versioned caches by build ID, switched navigations to network-first with a three-second timeout, cleaned up on activate, and showed a refresh prompt when a new worker was waiting. New releases reached 90% of active users within a day, and storage fell to under 20MB per user.
Measuring Update Propagation
Know how quickly releases reach users. Include the build ID in RUM beacons (read it from the page or from the controlling service worker via postMessage) and chart the share of sessions on the latest build in the days after each deploy. A healthy service worker setup reaches most active users within a day; a long tail of sessions on builds weeks old means workers are stuck waiting or HTML is served cache-first. The same data tells you how long to keep old hashed assets on the server — keep them at least as long as meaningful traffic remains on old builds.
Common Mistakes
- Caching sw.js with a long max-age. Browsers re-check service worker scripts at least daily, but long caching delays update detection; use
no-cache. - Deleting caches in install. Breaks old tabs mid-session.
- Precaching everything. Large precache lists waste bandwidth on every release.
- Never testing the update path. Most service worker bugs appear only during upgrades.
Edge Cases
Navigation preload. With network-first navigations, enabling navigation preload avoids the worker startup delay on each navigation — see enabling navigation preload in service workers.
Emergency kill switch. Ship a way to unregister the worker remotely (a "kill switch" sw.js that unregisters itself and clears caches) in case a broken worker ships.
Opaque responses. Cross-origin no-CORS responses cached by the worker have padded sizes and count heavily against quota.
Multiple apps on one origin. Scope workers and cache names per app so one app's cleanup does not delete another's caches.
FAQ
Does a CDN purge affect service worker caches?
No. Purges clear CDN caches only. The service worker keeps serving what it has stored until your code updates or deletes it.
How often do browsers check for a new service worker?
On navigations within scope and on certain events, with the worker script's HTTP cache bypassed after 24 hours at most. Serving it with no-cache ensures checks see changes immediately.
Should I use Workbox?
For most sites, yes. Workbox's precaching handles revisioning and cleanup correctly, and its strategies implement the patterns above. Custom workers are fine but easy to get subtly wrong.
What about users who never close their tabs?
Use an update prompt or periodic checks (registration.update()), and design the app so that an old version keeps working against the current API for a reasonable period.
Can I force all clients onto a new version immediately?
Calling skipWaiting and clients.claim and then reloading clients forces it, at the risk of interrupting users mid-task. Reserve forced updates for critical fixes.
How do I clean up storage from very old versions?
The activate handler deletes any cache not in the current keep-list, which removes all older versions at once, regardless of how many accumulated.
Related
- Handling service worker updates with skipWaiting — the update UX.
- Precaching vs runtime caching with Workbox — what to precache at all.
- Handling chunk load errors after a deploy — the related open-tab problem without a service worker.