Browser & Frontend Runtime
Service worker lifecycle and cache strategies
You deploy a new service worker, reload the page, and still get the old behaviour. No error — the new worker installed fine. It is just waiting for you to close every tab.
Registration and scope
navigator.serviceWorker.register('sw.js') starts the lifecycle. The scope defaults to the directory of sw.js — a worker at /app/sw.js controls all pages under /app/. You can narrow it: register('sw.js', { scope: '/app/checkout/' }).
A service worker has no DOM and no persistent global state — the browser kills and restarts it freely to save memory. All durable state lives in the Cache API or IndexedDB. It runs on its own thread, independent of any page — it can receive push notifications and run background sync even when no tab is open.
The lifecycle
register → install → [waiting] → activate → idle ↔ runninginstall — fires when the browser parses the new worker for the first time. This is where you pre-cache the app shell:
self.addEventListener('install', event => {
event.waitUntil(
caches.open('v1').then(cache => cache.addAll(['/app.js', '/index.html']))
);
});event.waitUntil(promise) tells the browser: do not advance the lifecycle until this promise settles. Forget it and the browser may kill the worker mid-cache-population.
waiting — a newly installed worker does not take control of already-open pages by default. It waits until every tab using the old worker closes. This prevents a page from running old HTML with new cached assets.
activate — fires after waiting. This is where you delete old caches:
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(keys =>
Promise.all(keys.filter(k => k !== 'v1').map(k => caches.delete(k)))
)
);
});fetch — intercepts every network request in scope. event.respondWith(response) determines what the page receives.
- install → activate
- Only after all old tabs close (without skipWaiting)
- Idle worker killed by browser
- Seconds after last event
- State check (DevTools)
- Application → Service Workers panel
- sw.js cache by browser (default)
- Up to 24 hours — use Cache-Control: no-cache
skipWaiting + clients.claim()
To activate immediately without waiting for tabs to close:
// install handler
self.skipWaiting();
// activate handler
self.addEventListener('activate', event => {
event.waitUntil(clients.claim());
});skipWaiting() — new worker activates immediately, even with open pages.
clients.claim() — new worker takes control of all open pages in its scope.
Use this deliberately. Without content-hashed asset filenames, skipWaiting is how you ship the version-mismatch failure mode.
Cache strategies
Inside the fetch handler you pick a strategy per request type:
Cache-first — check cache, fall back to network. Right for immutable, content-hashed static assets (app.4f3a1c.js):
event.respondWith(caches.match(event.request).then(r => r || fetch(event.request)));Network-first — try network, fall back to cache on failure. Right for API responses that should be fresh but must degrade gracefully offline.
Stale-while-revalidate — serve the cached copy immediately, fetch a fresh copy in the background to update the cache for next time. Right for content that can be slightly stale (avatars, feeds).
Network-only and cache-only are the degenerate ends.
Libraries like Workbox give these strategies declaratively with route matching, but the strategies themselves are ten lines of fetch-handler code.
Order the service worker lifecycle events from registration to steady state.
- 1 register(): browser downloads and parses sw.js
- 2 install event: pre-cache the app shell
- 3 waiting: new worker idles until old pages close (unless skipWaiting)
- 4 activate event: clean up old caches
- 5 fetch events: intercept and respond to requests
You deploy a new service worker, reload the page, and still get the old behaviour. Why?
Which job is a service worker the right tool for?
A page enables COOP + COEP to use multithreaded WASM. After deploy, the WASM module fails to start and the page is missing its hero image and a third-party analytics script. What happened?
Why this works
Why does the waiting state exist? Consider: you deploy version N+1 of your app. An open tab loaded version N’s HTML and has version N’s worker. If the new worker activated immediately, it would start serving version N+1’s cached assets to a page that was expecting version N’s assets — a version mismatch that could break the page mid-session. The waiting state prevents this: a user finishing work in an open tab will complete their session with a consistent asset set. skipWaiting is the escape hatch when you are confident your assets are content-hashed (old and new can coexist) and you want the new worker active immediately.
- 01Walk through the service worker lifecycle from register() to handling fetch events.
- 02What is the waiting state and how do you override it?
- 03What are the three main cache strategies and when do you use each?
A service worker intercepts every fetch request in its scope once activated. The lifecycle is: register, install (pre-cache), waiting (new worker idles until old tabs close), activate (clean old caches), then the idle/running fetch loop. event.waitUntil(promise) in any handler prevents the browser from killing the worker mid-operation. skipWaiting() + clients.claim() overrides the waiting state, activating immediately — safe only with content-hashed assets. Service workers have no persistent global state; the browser kills them aggressively and restarts on the next event, so all durable state must live in the Cache API or IndexedDB.
appears again in143
- Why GraphQL gets N+1junior
- DataLoader mechanics: tick-boundary batchingmiddle
- Batch function contracts: ordering, shapes, errorsmiddle
- Federation and lookahead: batching beyond DataLoadermiddle
- Query complexity defences: depth, cost, persisted queriesmiddle
- Senior GraphQL API: scheduling contract, tenant isolation, observabilitysenior
- Why idempotency: making retries safejunior
- Server-side state machine: four states of an idempotency keymiddle
- Outbox and inbox: effectively-once across the dual-write boundarymiddle
- Concurrency and cache architecture for idempotency at scalesenior
- Observability, production failures, and global-scale designsenior
- What is a cache stampede and why it makes things worsejunior
- Lock and single-flight: bounding concurrent rebuildsmiddle
- XFetch: coordination-free probabilistic early expirationmiddle
- Stale-while-revalidate and CDN request coalescingmiddle
- Detecting stampedes and designing TTL for productionmiddle
- Metastable failure, fencing tokens, and production postmortemssenior
- What a relation is: tables, rows, keys, and constraintsjunior
- Constraints, keys, and Postgres data typesmiddle
- Normal forms, denormalization, and why schemas stickmiddle
- JSONB, arrays, and when a side table winsmiddle
- Heap storage, TOAST, and column alignmentsenior
- Schema integrity: deferral, versioning, and production failure modessenior
- Relational vs document, wide-column, graph, and key-valuesenior
- Index-only scans, the Visibility Map, and INCLUDEsenior
- Production failure modes and the index audit playbooksenior
- pg_statistic, ANALYZE, and production observabilitymiddle
- Production failure modes and plan stabilitysenior
- MVCC: why readers and writers never wait for each otherjunior
- Row versions and snapshots: the on-disk mechanicsmiddle
- HOT updates and isolation levels: what you gain and what you paymiddle
- Vacuum and bloat: keeping the storage tax boundedmiddle
- CLOG, XID wraparound, and MultiXact: deep visibility internalssenior
- SSI internals and production autovacuum tuningsenior
- Real-world MVCC failures, deployment patterns, and distributed snapshotssenior
- Connection pools: amortising the cost of a Postgres backendjunior
- PgBouncer session, transaction, and statement modesmiddle
- Pool sizing: the (cores × 2) + spindles formula and the two-layer stackmiddle
- Pool exhaustion and idle-in-transaction: the 3 AM failure modemiddle
- Migrating to transaction mode: rollout playbook and PgBouncer 1.21 prepared statementsmiddle
- The Postgres process model and why raising max_connections degrades throughputsenior
- Pooler landscape 2026, serverless connection storms, and the full failure-mode taxonomysenior
- What a schema migration is and why it replaces ad-hoc DDLjunior
- ADD COLUMN: instant in PG 11+ vs rewrite in older Postgresjunior
- The lock-queue failure mode: why instant DDL can freeze the databasemiddle
- Safe DDL patterns: NOT VALID, CONCURRENTLY, and unsafe-op fixesmiddle
- Expand-contract: zero-downtime for breaking schema changesmiddle
- Advisory locks, migration tools, and deploy coordinationsenior
- Migration failure taxonomy and production disciplinesenior
- Why sharding exists: the single-Postgres ceilingjunior
- Shard-key selection: hash, range, list, and directory strategiesmiddle
- Partitioning vs sharding: same word, two different thingsmiddle
- Co-location and Citus: the invariant that makes sharding usablemiddle
- The hot-shard failure mode: detection, isolation, and durable policymiddle
- Schema-based sharding and multi-tenancy alternativessenior
- Online resharding, 2PC, and the operational cost of shardingsenior
- The seven acts: from CREATE TABLE to Citusjunior
- Acts 1–3 in depth: schema, indexes, and planner statisticsmiddle
- Acts 4–6 in depth: MVCC bloat, connection pooling, and safe migrationsmiddle
- Act 7 in depth: sharding, co-location, and the seven-tier tradeoff cascademiddle
- Observability, anti-patterns, and production triagesenior
- Raft roles, terms, and why majority quorums prevent split brainjunior
- How Raft replicates a log entry and decides it is safe to commitmiddle
- Raft leader election: timeouts, voting rules, and the four safety propertiesmiddle
- Raft in the real world: partitions, slow disks, and client routingmiddle
- Raft extensions: pre-vote, learners, snapshots, and linearizable readssenior
- Raft in production: membership changes, Multi-Raft, and observabilitysenior
- Where data fetching happens — and why it decides LCPjunior
- Fetch waterfalls — diagnosis and the Promise.all curemiddle
- React Server Components and Suspense streamingmiddle
- Client-side cache: TanStack Query, SWR, and stale-while-revalidatemiddle
- LCP, prefetch, and race conditions in interactive fetchingmiddle
- Senior internals: RSC payload, caching layers, and production failure modessenior
- The three-way handshakejunior
- Sequence numbers and connection statemiddle
- DNS: what it does and why it existsjunior
- The resolver walk: referrals, record types, and gluemiddle
- TTL, caching, and DNS propagationmiddle
- The 1-RTT handshake: key shares and ECDHEmiddle
- Session resumption and 0-RTTmiddle
- WebSocket: the HTTP upgrade handshakejunior
- WebSocket frame format: opcodes, masking, fragmentationmiddle
- WebSocket backpressure: when clients can''''t keep upmiddle
- Reconnection: jittered backoff, thundering herd, message resumptionsenior
- WebSocket at scale: HTTP/2 multiplexing, permessage-deflate, C10Msenior
- WebSocket in production: proxies, security, and distributed architecturesenior
- What reverse proxies dojunior
- Health checks, connection draining, and slow startmiddle
- Session affinity, consistent hashing, and the right fixmiddle
- Retry storms, circuit breakers, and load sheddingsenior
- Resilient LB architecture: anycast, zone-aware routing, and observabilitysenior
- Why QUIC and not TCP+TLSjunior
- Connection IDs and network migrationmiddle
- 0-RTT resumption and packet encryptionsenior
- DDoS: what it is and why it worksjunior
- Amplification attacks and state exhaustionmiddle
- Rate limiting: algorithms and architecturemiddle
- WAFs, firewalls, mTLS, and HSTSmiddle
- DNS cache poisoning and BGP hijackingsenior
- Defense-in-depth architecture and attack economicssenior
- DNS, TCP, TLS in sequence: where the milliseconds gomiddle
- Proxy intercepts and security gates: rate limiters, WAF, mTLSmiddle
- Alternate paths: QUIC 0-RTT, WebSocket upgrade, connection migrationmiddle
- Observability: distributed traces, USE/RED, and samplingsenior
- Resilience: cascading retries, circuit breakers, and error budgetssenior
- What the three signals are: logs, metrics, and tracesjunior
- Why structured logs exist: the diary vs the spreadsheetjunior
- The production log schema: fields every line must carrymiddle
- PII redaction and log injectionsenior
- OTel Logs Data Model and audit logs as a subsystemsenior
- SLI, SLO, and the error budget: reliability by the numbersjunior
- Error budget policy, latency SLOs, and composite journeysmiddle
- Production SLO failures, self-observability, security, and the big picturesenior
- The incident loop: from pager to postmortem to preventionmiddle
- Cache lines, struct layout, and false sharingmiddle
- SIMD, SoA vs AoS, and memory bandwidthmiddle
- Cache-oblivious algorithms, PGO, and production failuressenior
- GC in production: observability, security, edge cases, and fleet governancesenior
- Batching: amortize fixed cost per operationjunior
- The batching window: size and wait timemiddle
- Batching in Kafka and Postgresmiddle
- io_uring and observability of batchingmiddle
- From Nagle to io_uring: evolution of batchingmiddle
- Backpressure, failure isolation, and batch security in productionsenior
- CI enforcement and RUM: making budgets stickmiddle
- V8 JIT pipeline, HTTP priorities, and bundle securitysenior
- The performance loop: discipline, not a projectjunior
- Classify and fix: matching bottleneck families to remediesmiddle
- Observability stack and CI gates: catching regressions before they shipmiddle
- Incident to enforcement: SLO burn to verified fix in 35 minutesmiddle
- Culture, economics, and org-scale performancesenior
- At-most-once, at-least-once, exactly-once: the three delivery contractsjunior
- The three failure legs — where duplicates and losses actually happenmiddle
- Consumer-side dedup: the cheapest path to exactly-once processingmiddle
- Kafka exactly-once semantics: idempotent producer and transactionsmiddle
- SQS visibility timeout, DLQ, and the outbox patternmiddle
- Exactly-once in production: impossibility proof, hybrid patterns, and real incidentssenior
- What OAuth is and why passwords are not the answerjunior
- Authorization code flow with PKCEmiddle
- ID token validation and JWKS cache managementmiddle
- Refresh token rotation and scope-based least privilegemiddle
- Sender-constrained tokens: DPoP and mTLSsenior
- OAuth in production: audience attacks, observability, and real failuressenior