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.
Prerequisites checklist
Before writing a single set() call, confirm the following are wired up:
storagepermission declared inmanifest.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
localvssyncper 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 to design around
- Item size: 8 KB per key (
QUOTA_BYTES_PER_ITEM). Large blobs must be chunked or routed tostorage.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,Dateand 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
| Metric | Limit |
|---|---|
Per-item size (sync) | 8 KB |
Total sync storage | 100 KB |
| Write operations | ~120 / minute, ~1,800 / hour |
Total storage.local | 10 MB (unlimitedStorage removes the cap) |
Related
- Local vs sync storage performance comparison — choose the right area per key.
- Handling storage quota exceeded errors — recover from
QUOTA_BYTESgracefully. - Encrypting sensitive data in chrome storage — sync secrets safely.
- Message Passing Architecture — push changes between contexts.
- Identity & OAuth Authentication — the tokens that decide which storage area to use.
- Up to the Core APIs & Cross-Browser Data Management overview.