Service Worker Release Activation: Client Compatibility Handshakes, Cache Generations, and Safe Rollback
A successful Service Worker install does not mean it can immediately take over old pages; `skipWaiting` can also make legacy clients read caches built for a new protocol. This article controls activation with a complete release manifest, client handshakes, and generational cache cleanup.
Key Points
- Topic Focus: TypeScript、DevOps
- Suitable for: indie hackers, full-stack engineers, AI application developers
- Core value: quickly understand background, practical paths, common pitfalls, and engineering advice
Progressive Web Apps (PWA) introduces Service Workers, manifests, and offline caching, while Layered Web Caching Strategies explains Cache First and Network First. Production incidents, however, often occur at version boundaries: the page JS is still release 17, but worker 18 calls skipWaiting() and begins interpreting release 18 requests, or it deletes the old cache during activate and leaves pages that have not yet reloaded with a blank screen while offline. This article focuses only on worker releases and cache generations; it does not repeat the basics of PWAs.
One Manifest Binds Pages, Workers, and Assets
At build time, publish an immutable serviceWorkerRelease.v1 manifest that pins the worker script digest, page bundle/protocol versions, precache manifest root, runtime cache schema, minimum API version, range of client protocols that may be taken over, and rollback reader. Asset URLs must be immutable by content digest or release ID. Overwriting /latest.js while configuring a long cache lifetime can cause browsers to receive a mixture of artifacts.
schema: serviceWorkerRelease.v1
release: web-2026.07.19.18
workerDigest: sha256:41a...
clientProtocol: sw-client@5
compatibleClients: [4, 5]
precacheRoot: sha256:98c...
cacheSchema: runtime-cache@3
activationPolicy: DRAIN_OR_COMPATIBLE_CLAIM
The install phase writes only to precache:<releaseDigest>:staging. Every response must have the expected status, type, length, and digest; the final URL after a redirect must also appear in the manifest. Write the completion marker only after every object has been verified, allowing event.waitUntil() to determine whether installation succeeds. Cache Storage has no global transaction spanning multiple put() calls, so the marker can prove only that the immutable namespace is complete. The mere existence of a cache name does not mean it is ready. The multi-file digest constraints in Browser Wasm Asset Integrity apply equally to an App Shell.
Release states are DISCOVERED -> INSTALLING -> STAGED -> WAITING -> ACTIVATING -> ACTIVE -> DRAINING -> RETIRED. The browser's installing/waiting/active worker states are the source of truth for the lifecycle. The application receipt records the worker digest, complete cache root, activation policy, and observed client set; it must not fabricate a separate active pointer.
Before Activation, Ask Whether Old Pages Can Coexist
After loading, every page reports its clientInstanceId, page release, client protocol, number of uncommitted operations, and whether it can reload safely to the registration. If the waiting worker participates in the inventory, it must use clients.matchAll({ type: "window", includeUncontrolled: true }), then filter by registration scope/storage key and bind the receipt to the inventory nonce, observed client ID set, and individual ACKs. The default includeUncontrolled: false would give a waiting worker that does not yet control the old pages an empty or incomplete set.
Enumeration is not an atomic snapshot; a new client may join after the handshake. Consequently, inventory may support a call to skipWaiting() only when the release declares compatibility with every client protocol that remains supported. If an incompatible generation exists, keep the worker waiting by default until the old clients have drained. When a security fix cannot wait indefinitely, the server must first fence high-risk actions from old pages and require a reload. Any omission from the inventory must still be rejected by protocol checks on each fetch/message; a one-time “complete-set ACK” cannot become a permanent pass.
skipWaiting() changes only the waiting policy; it does not prove compatibility. clients.claim() changes the controller of current pages and is permitted only when the manifest declares compatibility and the page handles controllerchange. Fetch/message acceptance points must also validate the actual controller release, client protocol, and request schema. An unknown message from an old page returns CLIENT_RELOAD_REQUIRED; processing must not continue using the new version's default fields.
The active worker performs cache cleanup by namespace and client receipt. First stop writing to the old generation, wait for known old clients to drain, and then delete caches that are not referenced by the current or rollback release. Never execute caches.keys().map(delete) at the beginning of activate. Runtime-cached responses must also be bound to a schema and release, preventing a new worker from interpreting old JSON as the new format. Browser Storage Quota Recovery handles origin-level pressure; this protocol determines only which caches still have release references.
Validate with Mixed-Version Failures
Keep three old pages open simultaneously during testing and inject an interrupted install, a single asset returning 404, a CDN serving old bytes, a crash before the completion marker, a waiting worker restart, offline activation, a race between skipWaiting and an uncommitted form, a late old-version message after clients.claim, and a request from an old page after the new worker deletes the old cache. Assert that a failed install does not affect the current active worker; every fetch/message is handled only by a compatible combination; zero incomplete caches are adopted; and zero pages declared unsafe to reload lose state.
Release gates require 100% precache digest completeness, zero old/new protocol misdecodes, zero erroneous requests after a controller change, a 100% success rate for the offline golden path, a post-activation blank-screen rate no higher than the baseline, and convergence of unreferenced cache bytes to the budget within seven days. First install only into the waiting state and observe the handshake, then enable compatible claim for internal users, and finally expand the rollout. A rollback cannot “resurrect” an old worker once it is redundant. Instead, publish a new rollback release that reuses the last validated implementation and retained assets but has a new worker digest and monotonically increasing release sequence. A Feature Toggle can stop a new feature, but version handshakes and cache integrity cannot be disabled.
Further Reading
Related
Related Content
Article
Is Next.js App Router a Good Fit for a Technical Community?
Technical trends, engineering practices, and independent development perspectives centered on Next.js and React.
Article
SEO Design for a Content Community with Next.js
Technical trends, engineering practices, and an indie developer's perspective on Next.js and TypeScript.
Article
The Engineering Value of TypeScript in AI Application Frontends
Technical trends, engineering practices, and an independent developer perspective on TypeScript and React.
Article
What Value Do Server Components Offer Content Sites?
Technical trends and engineering practices around Next.js and React, from an independent developer's perspective.