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

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

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

References