Offline Storage and Service Worker Lifecycle
A service worker is a script the browser runs separately from your pages. It can intercept network requests, serve responses from caches, and keep working when the network does not — the foundation of offline-capable progressive web apps (PWAs). Combined with IndexedDB for structured data, it lets users open the app, read their data, and even make changes without a connection.
The hard parts are not the first version but every version after it. A service worker has a lifecycle that deliberately keeps old code running until it is safe to switch, so users can end up on stale versions for days, or stuck on a broken one. Caches grow until the browser evicts them. IndexedDB schemas change between releases while old tabs are still open. This page covers the lifecycle, update flows, storage migrations, and recovery paths that keep offline apps trustworthy.
TL;DR
- A service worker goes through install → waiting → activate, then handles fetch events for pages in its scope.
- A new version waits until no tabs use the old one, unless it calls
skipWaiting()— usually after the user agrees to update. - Version your caches, precache during install, and delete old caches during activate.
- Choose a caching strategy per request type: cache-first for versioned assets, network-first for navigations and API data.
- Version IndexedDB schemas with step-by-step migrations and handle blocked upgrades from other tabs.
- Check quotas, request persistent storage when data matters, queue offline writes with idempotency keys, and keep a kill switch.
Quick Example
A service worker with versioned precaching, cache cleanup, and an opt-in update:
In the page: offer the update, and reload once the new worker takes control:
An IndexedDB schema with step-by-step migrations, using the idb library:
Core Concepts
The Lifecycle
The browser checks for a new worker on navigation and periodically; any byte difference in the script (or its imported scripts) starts a new install. The service worker script itself bypasses the HTTP cache for update checks by default, but keep it uncached at the CDN to make updates prompt.
Updates Without Surprises
The waiting phase exists so one tab never runs old page code against a new worker. skipWaiting() cuts the wait short, which is safe only if old pages can work with the new worker's caches and API assumptions. Most apps prompt the user ("A new version is available — reload"), call skipWaiting() on acceptance, and reload once on controllerchange. Guard the reload so the first install does not trigger it, and so it cannot loop.
Caching Strategies
Libraries such as Workbox implement these strategies, precache manifests, and cache expiration. Never cache authenticated responses in shared caches or across users on shared devices. See HTTP Caching, Cookies, and Proxies.
Offline Data in IndexedDB
Schema Versions and Migrations
IndexedDB databases have an integer version. Opening with a higher version runs an upgrade callback, the only place object stores and indexes can change. Write migrations as cumulative steps keyed on oldVersion, so users who skipped releases still upgrade correctly, and transform existing records inside the upgrade transaction when shapes change.
An upgrade cannot proceed while another tab holds a connection to the old version: that tab receives a versionchange event (idb's blocking callback) and should close its connection or ask the user to reload; the upgrading tab receives blocked until it does.
Quotas, Persistence, and Eviction
Treat browser storage as a cache of server data unless the app is designed as local-first with sync. See Local-First Software.
Offline Writes
Queue changes in an outbox store with a client-generated idempotency key per request, show them as pending in the UI, and replay them when connectivity returns (on the online event, on app start, or with Background Sync where supported). The server must deduplicate by key and handle conflicts.
Offline UI and Recovery
- Design explicit offline, syncing, and failed states;
navigator.onLinereports a network interface, not real connectivity, so also react to failed requests. See Loading, Empty, and Error States. - Show data freshness ("Last updated 2 hours ago") when serving cached content.
- Keep a kill switch: a minimal replacement service worker that deletes caches and unregisters itself, ready to deploy at the same URL if a bad worker ships. The
Clear-Site-Dataresponse header can also clear caches and storage. - Test offline behavior and updates with browser DevTools (offline mode, "update on reload", storage inspection) and end-to-end tests.
Best Practices
Version Everything
Version caches, precache manifests, and database schemas, and clean up old versions deliberately.
Ask Before Switching Versions
Prompt users to update rather than calling skipWaiting() unconditionally, unless every version is backward compatible.
Keep Writes Out of Caches
Never serve POST, PUT, or DELETE from caches; queue them in IndexedDB with idempotency keys.
Prepare a Kill Switch
Keep a no-op, self-unregistering service worker ready, and know how to deploy it quickly.
Common Mistakes
Caching the Service Worker Script at the CDN
Unconditional skipWaiting
Migrations That Assume the Previous Version
FAQ
Why doesn't my new service worker take effect?
A new worker waits until no open pages are controlled by the old one. Close all tabs of the app, use DevTools' "update on reload" during development, or prompt users and call skipWaiting() when they accept.
Should I use the Cache API or IndexedDB?
The Cache API for request and response pairs — pages, scripts, images, API responses. IndexedDB for structured application data, queues, and anything you query or update.
How much data can a web app store offline?
It depends on the browser and device; quotas are generally a share of available disk space. Use navigator.storage.estimate() to check and navigator.storage.persist() to reduce eviction risk.
How do I handle IndexedDB schema changes?
Increase the database version and implement cumulative migration steps in the upgrade callback based on the old version, and handle blocked upgrades when other tabs keep the old version open.
How do I recover from a broken service worker?
Deploy a replacement worker at the same URL that deletes caches and unregisters itself, or send the Clear-Site-Data header, then deploy a fixed version.
Related Topics
- Progressive Web Apps — Installable, offline-capable web apps
- Local-First Software — Data that lives on the device and syncs
- Data Fetching — Loading and caching data in the frontend
- HTTP Caching, Cookies, and Proxies — Caching rules on the network side
- Loading, Empty, and Error States — Designing offline and failure states
- Web Performance — Fast loading with and without the network