Summary
An enterprise-grade Trusted Web Activity (TWA) deployment exhibited inconsistent UI states across multiple “clean” Android devices. Despite all devices having no local cache and no prior history with the URL, a subset of users received a stale version of the Progressive Web App (PWA), while others received the latest version. This investigation concludes that the issue was not local device storage, but a race condition between Service Worker lifecycle management and Global CDN propagation.
Root Cause
The inconsistency stemmed from a combination of two distributed systems phenomena:
- CDN Propagation Lag: During a deployment, the global Content Delivery Network (CDN) does not update all Edge nodes simultaneously. Some nodes served the new manifest and assets, while others served the legacy version.
- Service Worker “Wait” State: When a TWA launches, the browser engine checks for a new Service Worker. If a device hits an Edge node serving a “new” version of the
sw.jsfile, but that file points to assets still residing on an “old” Edge node, the Service Worker enters a broken or inconsistent state. - Atomic Deployment Failure: The deployment was not atomic. The HTML/index file was updated before the supporting JS bundles and the Service Worker script were fully propagated across all geographic regions.
Why This Happens in Real Systems
In distributed architectures, consistency is not instantaneous. This phenomenon is a manifestation of the CAP Theorem, where systems often trade strict consistency for availability and partition tolerance.
- Eventual Consistency: CDNs are designed for eventual consistency. There is always a window of time where
Node Ahas version 2.0 andNode Bhas version 1.0. - Cache-Control Headers: If the
sw.jsfile has a highmax-ageor if theindex.htmlis cached at the Edge, the TWA will continue to request the old dependency tree. - Network Partitioning: A device on a specific mobile carrier might be routed to a specific ISP Edge node that hasn’t received the purge command, while another device on a different carrier hits a fully updated node.
Real-World Impact
- User Trust Erosion: Users seeing different UIs on the “same” app creates a perception of instability and poor quality.
- Data Corruption: If a stale UI sends requests to a new API schema (or vice-versa), it can lead to malformed state transitions or failed transactions.
- Support Overhead: Engineering teams waste hundreds of hours chasing “ghost bugs” that are impossible to reproduce in a local development environment.
Example or Code
// The dangerous pattern: Updating the manifest before the assets
// Deployment Script (Pseudocode)
async function deploy() {
await uploadAssets('/dist/v2'); // Step 1
await uploadServiceWorker('/sw.js'); // Step 2
await purgeCDN(); // Step 3: This takes time!
}
// The correct pattern: Versioned URLs and aggressive SW invalidation
// service-worker.js
self.addEventListener('install', (event) => {
// Force immediate activation to prevent "waiting" state
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil(clients.claim());
});
How Senior Engineers Fix It
To solve this, we move away from “hope-based” deployments toward immutable infrastructure and versioned deployments:
- Content-Addressable Hashing: Never deploy
main.js. Always deploymain.ae3f21.js. This ensures that if the HTML points to a new file, it is impossible to load the old one. - Atomic Purging: Use a deployment pipeline that ensures all assets are uploaded and verified before the
index.htmlor thesw.jsis updated. - Service Worker “Kill Switch”: Implement a mechanism in the Service Worker to detect version mismatches and force a
window.location.reload()if the assets don’t align with the expected manifest. - Cache-Control Strategy: Set
Cache-Control: no-cache, no-store, must-revalidatespecifically for thesw.jsandindex.htmlfiles to ensure the entry points are always checked against the origin.
Why Juniors Miss It
- Local Environment Bias: Juniors often test using
localhostor a single staging server where consistency is 100%. They do not account for the distributed nature of the Internet. - Assumption of Synchronicity: They assume that once a “Deploy” button is pressed, the entire world sees the change at the same millisecond.
- Focus on Local Cache: They spend time clearing Chrome/WebView cache on the device, failing to realize that the malicious actor is the server-side Edge node, not the client-side storage.