Service Worker Cache Drift: Diagnosing Stale HTML After Deployments
A practical SEO method for separating stale HTML served by a browser service worker from problems in the origin, CDN, application cache or rendering layer.
A production deployment can be correct at the origin while some users continue to receive an older HTML document. The symptom may resemble an ordinary caching problem: an old title, missing canonical, outdated internal links or a route that behaves as though the previous release is still live.
One possible cause sits inside the browser. A registered service worker can intercept document requests for pages it controls. Its fetch handler may return a response from the Cache API rather than forwarding the request to the network. The result is a stale browser experience even though a direct request, server-side check or CDN inspection shows the new release.
This article sets out a diagnostic method for separating service-worker state from upstream cache state. It covers what to inspect, how to compare controlled and bypassed sessions, and how to validate existing tabs alongside fresh browser clients after a release. The aim is not to prove that a browser symptom has affected Googlebot. It is to establish which layer is serving which document, then assess the SEO-visible difference accurately.
What service-worker cache drift means
A service worker is a script registered for a site and scope. Once installed and active, it can control matching pages and handle their fetch events. Its fetch handler may pass a request to the network, construct a response or read a stored response from a named Cache API cache. The Service Worker API documentation describes the browser-side model and the relationship between registrations, workers and controlled clients.
For this article, service-worker cache drift means that a registered worker serves an older response after a new release has been deployed, typically by selecting an older response from the Cache API. The response might contain:
- an old HTML shell or route document;
- an outdated title, meta description or canonical element;
- old robots directives or hreflang annotations;
- structured data from the previous release;
- obsolete internal links or navigation;
- a previous route status, fallback page or application shell.
This differs from stale HTML returned by an origin, reverse proxy or CDN. It also differs from the browser's ordinary HTTP cache. The Cache API is named origin storage that service-worker code can open, match, populate and delete. It does not automatically behave like the browser's HTTP cache, and old entries generally remain until the application removes them or stops selecting them. See the Cache interface documentation for the underlying storage methods.
The distinction matters because a direct request can be current while a controlled browser navigation is not. Testing only the origin therefore cannot rule out a client-side response layer.
Why a deployment can leave an old worker in control
Service-worker updates follow a lifecycle rather than taking effect as soon as a new script is uploaded. A new worker can install and enter a waiting state while an older active worker continues controlling existing clients. The lifecycle includes installing, waiting, activating, active and redundant states. The service-worker lifecycle guidance explains why an existing tab may continue to use an older worker after a new version is available.
Two methods often appear in release implementations:
self.skipWaiting()can ask a newly installed worker to move through the waiting phase without waiting for the usual client-closure condition.self.clients.claim()can allow an active worker to take control of matching clients that are not currently controlled by another worker.
These methods can reduce the time an older worker remains active, but they are not a complete deployment strategy. Immediate activation can combine a page loaded from one release with a worker from another. That mixed-version state may be harmless for a static asset, but it can be risky when the worker changes document routing, response formats or cache selection.
Inspect the lifecycle rather than assuming that a changed worker file means every tab is using it. Establish:
- which script URL is registered;
- what scope the registration covers;
- whether an installing or waiting worker exists;
- which worker is active;
- whether the current page has a controller;
- whether the fetch handler intercepts navigations or only static assets.
Start with a release baseline
Before changing browser state, record what the release is supposed to serve. Use a small set of representative URLs rather than relying on one homepage. Include a route whose metadata or routing behaviour changed, a route with important internal links and, if relevant, a page containing structured data or international annotations.
For each URL, save a baseline containing:
- HTTP status and final URL;
- the direct response body or a stable content hash;
- title and meta description;
- canonical and robots directives;
- hreflang links;
- structured-data blocks;
- important internal links;
- route-specific fallback or error behaviour.
A command-line request, or another request made outside the browser, provides a useful control condition. It cannot exercise a visitor's registered service worker, although cookies, authentication, headers, locale and upstream cache state can still make it differ from a browser request. Treat it as a server-side control, not proof that every browser receives the same response.
Use a layered comparison, not a single refresh
The most useful diagnostic is a controlled comparison. Record the browser context, worker state, URL, response content and SEO-visible fields for every test. A recommended sequence is:
- Retained normal session. Use the existing browser profile and tab, with the service worker operating normally. Capture the document and note the current controller, worker version and any stale fields.
- Bypassed service worker. In browser developer tools, enable the service-worker bypass control and repeat the navigation or request. Keep ordinary browser-cache settings recorded separately: bypassing a service worker is not the same as disabling the HTTP cache.
- Fresh worker-enabled context. Open a new browser context or profile with service workers allowed. Let the current worker install and, where applicable, control the page, then repeat the test after the relevant lifecycle events.
- Worker-blocked context. Use a separate context in which service workers are blocked. This provides a useful browser control, particularly for automated checks.
- Direct HTTP control. Request the same URL outside a browser service-worker context and compare the response with the browser observations.
The pattern is more informative than any individual result:
- If the retained session is stale but the bypassed, worker-blocked and direct responses are current, service-worker control, Cache API selection or update timing becomes the leading explanation.
- If both normal and bypassed sessions are stale, investigate the CDN, reverse proxy, application or template cache, build artefacts and routing state before blaming the worker.
- If only the fresh worker-enabled context is stale, the newly installed worker or its cache-population logic may be selecting an old response.
- If the direct request is current but all browser contexts are stale, check request variation and browser HTTP caching as well as service-worker behaviour.
- If the initial HTML is current but the post-execution DOM is old or inconsistent, investigate application rendering separately. A changed DOM does not by itself prove that the document response was stale.
This matrix is Plus IQ's recommended diagnostic method, not a browser-standard test. Its value is that it treats service-worker state as a separate layer from origin and upstream state. Keep the URL, release identifier, cookies, locale, headers and timing as consistent as possible so the comparison does not introduce a second explanation.
Inspect the registration and the selected cache
Browser developer tools are usually the quickest place to begin. In Chrome, the Application panel exposes service-worker registrations, worker status and Cache Storage, while the Network panel can provide information about request handling. Chrome's Application tooling documentation describes these controls, including service-worker inspection and Cache Storage views. The exact labels and workflow are browser-specific.
For a first pass in the page console, inspect the registration and controller:
const registration = await navigator.serviceWorker.getRegistration();
console.log({
scope: registration?.scope,
scriptURL: registration?.active?.scriptURL,
activeState: registration?.active?.state,
installingState: registration?.installing?.state,
waitingState: registration?.waiting?.state,
controller: navigator.serviceWorker.controller?.scriptURL
});
This is illustrative rather than a complete diagnostic. A missing registration does not prove that no worker affected an earlier navigation, and an existing registration does not prove that the tested URL is in scope or that a fetch handler selected a cached response.
Next, inspect Cache Storage and compare cached documents with the stale browser response:
const cacheNames = await caches.keys();
for (const name of cacheNames) {
const cache = await caches.open(name);
const requests = await cache.keys();
console.log(name, requests.map(request => request.url));
}
Look for:
- release-specific cache names or version markers;
- the affected document URL, including query-string variants;
- an older HTML body matching the observed title, canonical or links;
- old fallback responses or route shells;
- caches that should have been deleted during activation but remain present.
An old entry is not enough to establish causation. It may be historical residue that the worker never selects. The evidence is stronger when the document matches the cached response, the request is shown as service-worker handled, bypassing returns the new document and the worker's matching logic can select that entry. If necessary, inspect the worker's fetch handler or add temporary release instrumentation that records the cache name and response path.
Connect the response difference to SEO
Once a stale document is confirmed, compare it field by field with the release baseline. The differences should not all be treated as the same type of indexing problem.
- Title and meta description: these can change the snippet or page presentation, but a stale browser response is not evidence that a crawler received it.
- Canonical: an old canonical can point to a previous URL or release structure and deserves priority in triage.
- Robots directives: an outdated
noindex,nofollowor other directive can have a materially different consequence from an old description. - Hreflang: stale language or regional annotations can create incorrect cross-market signals, particularly when route structures have changed.
- Structured data: old schema can describe a product, article or organisation differently from the current page.
- Internal links: an old navigation or linking structure can affect what a user discovers and what the application exposes through the document.
- Route status and fallbacks: a cached application shell or fallback can make a route appear available in the browser when the deployed server response is different.
These differences are SEO-relevant by inspection, but their search consequences are field-specific. Google documents that crawling and rendering use HTML and rendered output to process content, metadata and links; its JavaScript SEO guidance provides useful context. It does not establish that Googlebot reuses a visitor's service-worker registration or Cache API entries. A browser failure should therefore not automatically be described as an indexing failure. Confirm crawler impact separately through logs, inspection tools or other controlled evidence.
Validate the release, including existing tabs
Fixing the stale response is not the same as validating the deployment. The release process should make worker and cache state observable. Versioned worker assets, versioned cache names or equivalent release markers make it easier to identify which code is active and which cache is being selected. The exact naming scheme is an implementation choice.
Release validation should cover:
- Installation: the intended worker script is fetched and reaches the expected lifecycle state.
- Activation: the old worker is replaced according to the application's policy, and any remaining waiting state is understood and intentional.
- Cache cleanup: obsolete caches are removed or demonstrably no longer selected.
- Client takeover: existing pages behave as intended when
clients.claim()is used, or after the controlled navigation expected by the application. - Existing tabs: a tab opened before deployment is refreshed and tested without assuming that a new tab represents the same state.
- Fresh sessions: a new context installs the current worker and receives the release baseline.
- Rollback: reverting the application does not leave a newer worker selecting incompatible caches or serving documents that no longer match the rolled-back release.
Automation can make these checks repeatable. Playwright documents both normal service-worker-enabled contexts and a separate option for blocking service workers. Its service-worker guidance also notes an important limitation: ordinary request interception may not observe requests already handled by a service worker. Capture browser-visible responses and worker state as well as network events.
For a deployment gate, assert the actual SEO fields rather than only a status code. Compare title, canonical, robots directives, hreflang, structured data and key links against the release baseline. Where a route's behaviour changed, assert the expected status and fallback response too.
Keep alternative explanations in view
Service-worker evidence should narrow the diagnosis, not end it prematurely. Similar symptoms can come from:
- CDN or reverse-proxy responses;
- application, template or fragment caching;
- an incomplete build or old static artefact;
- browser HTTP cache;
- cookies, authentication, locale or request headers;
- JavaScript changing the DOM after the initial response;
- back-forward cache restoring an earlier page state;
- routing or fallback logic that returns a different document or status.
If the page remains stale when service workers are bypassed or blocked, service-worker state is less likely to be the primary cause. If only the normal retained session is stale, inspect the controller, lifecycle and cache-selection path before making changes to upstream caching.
Definition of done
For SEO release validation, the issue is complete when the intended HTML is served in both fresh and previously controlled sessions; the expected worker is active according to the application's policy; obsolete caches are no longer selected; and critical SEO fields match the release baseline.
That baseline should include the fields that matter for the affected templates: title, canonical, robots directives, hreflang, structured data, internal links and route status where relevant. Record the tested worker version, cache name, browser context and release identifier so a later rollback or deployment can be compared against the same evidence.
The distinction is simple: an origin response can be current while a controlled browser response is not. Treat service-worker state as its own diagnostic layer, prove which response each client received and keep browser evidence separate from claims about crawler behaviour. For related checks, see our guides to rendering parity, template fingerprinting for deployment drift and distinguishing indexing lag from a discoverability defect.
Share this article