Chrome Storage API & Sync

Persist and synchronise extension state across devices with chrome.storage.sync — quotas, async patterns, change events, encryption and cross-browser adapters for Manifest V3.

Every Manifest V3 extension faces the same hard constraint: the background service worker is evicted after roughly 30 seconds of inactivity, taking all in-memory state with it. The chrome.storage API is the durable layer that survives that eviction, and chrome.storage.sync extends it across every device the user is signed into. Master this and your extension’s preferences, auth flags and cached configuration stay coherent whether the worker is alive, asleep, or running on a second laptop. This guide is part of Core APIs & Cross-Browser Data Management.

The gotcha that bites first: chrome.storage.sync is not a bigger localStorage. It is asynchronous, quota-limited to 8 KB per item and 100 KB total, rate-limited per minute, and the payload is transmitted to Google’s servers. Treat it as a small, slow, replicated key-value store and design around those limits from the first commit.

chrome.storage.sync data flow across extension contexts and devicesThe popup, content script and service worker all read and write through chrome.storage.sync, which persists to disk and replicates asynchronously to the same user's other signed-in browser profiles.PopupUI contextContent scriptisolated worldService workerbackgroundchrome.storage.syncasync · disk-backed8 KB/item · 100 KB capSync backendGoogle accountOther devicesauto-replicated

Prerequisites checklist

Before writing a single set() call, confirm the following are wired up:

  • storage permission declared in manifest.json — without it every call throws synchronously.
  • A versioned schema strategy, because synced data outlives your extension’s releases and a v2 build will read v1 shapes.
  • A decision on local vs sync per key — see the local vs sync storage performance comparison for the latency and quota trade-offs.
  • A plan for quota failures, covered in handling storage quota exceeded errors.

1. Declare the permission

The storage permission is not a host permission and triggers no install-time warning, so there is no reason to defer it.

1{
2  "manifest_version": 3,
3  "name": "Sync Demo",
4  "permissions": ["storage"] // grants storage.local, storage.sync and storage.session
5}

Execution context: Root manifest.json, parsed by the extension host at install time. Identical key on Chrome, Edge and Firefox; Safari accepts it but enforces its own iCloud-backed sync quotas.

2. Asynchronous read and write

Every chrome.storage method returns a Promise in MV3. Batch related keys into one object to collapse multiple IPC round-trips into a single write, and never assume a write has landed until its Promise resolves.

 1export async function syncUserPreferences(prefs: Record<string, unknown>) {
 2  try {
 3    await chrome.storage.sync.set({ userPrefs: prefs, version: 3 });
 4  } catch (error) {
 5    console.error("Sync write failed:", error);
 6    throw error; // surface quota/rate errors to the caller
 7  }
 8}
 9
10export async function loadUserPreferences(): Promise<Record<string, unknown>> {
11  const { userPrefs } = await chrome.storage.sync.get("userPrefs");
12  return userPrefs ?? {};
13}

Execution context: Runs in the service worker or any extension page holding the storage permission. await yields the event loop rather than blocking it. Firefox exposes the same surface under browser.storage.sync with native Promises; Chrome’s chrome.* namespace became Promise-based in MV3.

3. React to cross-context changes

A write in the popup must be visible to the service worker and any open options page. Instead of polling, subscribe to chrome.storage.onChanged and filter by areaName. This is the backbone of reactive extension UIs and pairs naturally with the message passing architecture when a context needs a push rather than a pull.

1chrome.storage.onChanged.addListener((changes, areaName) => {
2  if (areaName !== "sync") return;
3  if (changes.userPrefs) {
4    const next = changes.userPrefs.newValue;
5    broadcastStateUpdate(next); // re-render any live surfaces
6  }
7});

Execution context: Register at the top level of the service worker so it re-binds on every cold start. The listener fires in every extension context simultaneously — guard against feedback loops. Firefox and Safari fire the same event; Safari may coalesce rapid changes into a single callback.

MV3 Constraints box

  • Item size: 8 KB per key (QUOTA_BYTES_PER_ITEM). Large blobs must be chunked or routed to storage.local.
  • Total sync: 100 KB across all synced keys (QUOTA_BYTES).
  • Write rate: ~120 writes/minute and ~1,800/hour; bursts beyond this reject with MAX_WRITE_OPERATIONS_PER_MINUTE.
  • No globals survive eviction: the worker dies after ~30s idle, so storage — not a module-scope variable — is your source of truth.
  • Structured-clone only: values must be JSON-serialisable; Map, Set, Date and class instances are flattened or rejected.

Cross-browser notes

Firefox and Edge expose the identical API under the browser.storage.sync namespace and enforce their own per-extension sync quotas. Safari maps sync storage onto iCloud and is the most likely to throttle or silently cap. A thin runtime adapter removes the namespace branch from your call sites:

 1const storage = (typeof browser !== "undefined" ? browser : chrome).storage;
 2
 3export async function safeSyncWrite(data: Record<string, unknown>) {
 4  try {
 5    await storage.sync.set(data);
 6  } catch (err) {
 7    if (String((err as Error).message).includes("QUOTA_BYTES")) {
 8      await handleQuotaExceeded(data);
 9    }
10    throw err;
11  }
12}

Execution context: Shared utility module imported by every context. Resolves the chrome vs browser namespace at runtime so the rest of the codebase stays vendor-neutral. For a full divergence table see the cross-browser API compatibility reference.

Security and sensitive data

Sync storage is replicated through the user’s account, so never write tokens, PII or secrets in clear text. Keep device-bound secrets in chrome.storage.local, and where you must sync sensitive material, encrypt it first — the full pattern lives in encrypting sensitive data in chrome storage.

Quota reference

MetricLimit
Per-item size (sync)8 KB
Total sync storage100 KB
Write operations~120 / minute, ~1,800 / hour
Total storage.local10 MB (unlimitedStorage removes the cap)