Chrome Storage Sync API Guide for Extensions (2026)

Chrome Storage Sync API Guide for Extensions (2026)

Almost every extension needs to remember something — a preference, a toggle state, a list of rules the user configured. Chrome gives you four storage areas to do it with, and picking the wrong one produces bugs that only appear on a second device, a month later, at the moment a user hits a quota nobody read about.

This chrome storage sync api guide covers what chrome.storage.sync actually does, the exact limits it enforces, how it resolves conflicts, when to reach for local or session instead, and the handful of patterns that keep settings code from becoming a support burden. Everything here targets Manifest V3, which is the only manifest version accepting new submissions.

The Four Storage Areas and What Each Is For

Chrome exposes four areas under a single chrome.storage namespace. They share an API surface and differ entirely in lifetime and scope, and no chrome storage sync api guide is useful without the comparison, because half of all sync bugs are really "this data was never supposed to sync" bugs.

chrome.storage.sync — persisted and replicated across every device the user is signed into with sync enabled. Small quota, rate limited, JSON-serializable values only. Correct for user preferences.

chrome.storage.local — persisted on this machine only. Roughly 10 MB, expandable with the unlimitedStorage permission. Correct for caches, large data, and anything device-specific.

chrome.storage.session — held in memory for the lifetime of the browser session. Cleared when Chrome closes. Correct for transient state that must survive service worker termination but not a restart, such as an in-progress operation or a decoded token.

chrome.storage.managed — read-only, populated by enterprise policy. Correct for administrator-set defaults in managed deployments.

All four require one line in the manifest:

{
  "permissions": ["storage"]
}

That permission produces no user-facing warning at install time, which is one reason storage is a cheap dependency compared to almost anything else an extension can request.

The Quotas That Actually Matter

Any honest chrome storage sync api guide starts with the numbers, because the sync area is much smaller than developers expect.

ConstantValueMeaning
QUOTA_BYTES102,400Total bytes across all items in sync
QUOTA_BYTES_PER_ITEM8,192Maximum size of a single item
MAX_ITEMS512Maximum number of keys
MAX_WRITE_OPERATIONS_PER_HOUR1,800Sustained write ceiling
MAX_WRITE_OPERATIONS_PER_MINUTE120Burst write ceiling

For comparison, chrome.storage.local allows roughly 10 MB with no per-item limit and no write rate limit, and chrome.storage.session allows roughly 10 MB in memory.

Two details about how bytes are counted:

  • Size is measured on the JSON-serialized string of the value plus the length of the key. A key named userPreferencesForAdvancedMode costs thirty bytes on every write, forever. Short keys are not premature optimisation at a 100 KB ceiling.
  • The per-item cap is the one people hit first. 8 KB sounds generous until someone stores an array of user-defined rules in a single key. At around forty rules with a URL pattern and a label each, that key exceeds the limit and every subsequent write fails.

You can measure current usage directly:

const bytes = await chrome.storage.sync.getBytesInUse(null);
console.log(bytes, 'of', chrome.storage.sync.QUOTA_BYTES);

Passing null returns total usage. Passing a key or array of keys returns usage for those keys only. Logging this during development catches quota problems before users do.

Reading and Writing: The Manifest V3 Shape

The mechanics are small enough that the whole read-write surface fits in one screen. In Manifest V3 every storage method returns a Promise when you omit the callback. The callback form still works, but promises are the shape to write new code in — any chrome storage sync api guide written against the older callback style is describing a pattern you no longer need.

// Write
await chrome.storage.sync.set({ theme: 'dark', autoCopy: true });

// Read specific keys with defaults
const prefs = await chrome.storage.sync.get({ theme: 'light', autoCopy: false });

// Read everything
const all = await chrome.storage.sync.get(null);

// Remove
await chrome.storage.sync.remove('autoCopy');

// Nuke
await chrome.storage.sync.clear();

The most useful idiom in that block is passing an object to get. Chrome treats the object as a set of keys with default values and returns the stored value when present, the default when not. That eliminates the null-checking layer most extensions write by hand, and it keeps defaults in one place instead of scattered across every call site.

Values must be JSON-serializable. Objects, arrays, strings, numbers, and booleans survive the round trip. A Date becomes an empty object, a Map becomes an empty object, a Set becomes an empty object, and undefined values are dropped. Store timestamps as numbers and collections as arrays.

Reacting to Changes Across Devices

The event that makes sync worth using is chrome.storage.onChanged. It fires in every extension context — service worker, popup, options page, content scripts — whenever any area changes, including when a change arrives from another device.

chrome.storage.onChanged.addListener((changes, areaName) => {
  if (areaName !== 'sync') return;
  for (const [key, change] of Object.entries(changes)) {
    console.log(key, change.oldValue, '->', change.newValue);
  }
});

Three things worth internalizing about this listener:

  • It fires for remote changes. A user flipping a setting on their laptop triggers the listener on their desktop. This is the entire point of the sync area, and it means your UI should render from storage rather than from local component state.
  • oldValue is absent for newly created keys and newValue is absent for deleted ones. Check for presence rather than assuming both.
  • It does not fire for writes that do not change anything. Writing the same value twice produces one event, not two.

An options page that listens for changes and re-renders is a few lines longer than one that does not, and it is the difference between settings that feel synced and settings that require a reload to appear.

Conflict Resolution: Last Write Wins, Per Key

Chrome does no merging. When two devices write the same key while one is offline, the write that reaches the server last is the value that survives. There is no callback, no conflict event, and no way to intervene.

This has one direct architectural consequence: structure settings as many small keys, not one large object.

The tempting design is a single settings key holding an object with every preference in it. It reads nicely and it is wrong for three separate reasons:

  1. Conflicts destroy unrelated settings. Device A changes the theme, device B changes the shortcut, last writer wins, and one of those changes vanishes even though they never touched the same field.
  2. Every write costs the full object size. Toggling one boolean rewrites all 6 KB, burning quota and write operations.
  3. The 8 KB per-item cap becomes a global ceiling on all settings combined instead of a per-setting one.

Splitting into theme, shortcut, enabledSites, and so on gives per-key conflict granularity, cheap writes, and headroom. The cost is slightly more verbose reads, which the object-with-defaults form of get absorbs anyway.

Where a genuinely large collection is involved — a user rule list, a set of saved items — either shard it across numbered keys with a small index key, or accept that it belongs in chrome.storage.local and does not sync.

Rate Limiting and the Debounce Pattern

120 writes per minute sounds like plenty until you wire a text input directly to storage. An input event listener that calls set on every keystroke exhausts the minute budget in about ten seconds of typing, and every subsequent write rejects with a quota error until the window resets.

The fix is a debounce, and it belongs in every options page:

let pending;
function saveSetting(key, value) {
  clearTimeout(pending);
  pending = setTimeout(() => {
    chrome.storage.sync.set({ [key]: value });
  }, 500);
}

Half a second is a reasonable default. The user cannot tell the difference, and the write count drops by an order of magnitude.

Handle rejection explicitly rather than letting it disappear into an unhandled promise:

try {
  await chrome.storage.sync.set({ rules: nextRules });
} catch (error) {
  // QUOTA_BYTES_PER_ITEM exceeded, rate limit hit, or sync unavailable
  await chrome.storage.local.set({ rules: nextRules });
}

Falling back to local on a sync failure is a sound pattern for anything the extension genuinely needs to keep. The setting stops following the user between devices but does not silently vanish.

Sync vs. Local vs. Session: Choosing Correctly

This is the decision a chrome storage sync api guide exists to settle. It fits in a short table, and getting it right at the start avoids a migration later.

DataAreaWhy
User preferences and togglessyncSmall, and users expect them to follow
Keyboard shortcut configurationsyncTiny, and device-portable by nature
A cached API responselocalToo large, and device-specific anyway
Onboarding completed flagsyncUsers should not be onboarded twice
A per-device path or window sizelocalMeaningless on another machine
In-progress operation statesessionMust survive worker termination, not restart
Auth tokens and secretsnone of theseSee the next section
Enterprise defaultsmanagedRead-only, set by policy

The rule of thumb: if it would annoy the user to configure it twice, it belongs in sync. If it would be wrong on another machine, it belongs in local.

A related question is what to do about localStorage, the web platform API. Do not use it in an extension. It is synchronous, unavailable in service workers, not covered by the extension storage quotas, and cleared by browsing data operations in ways users do not expect. The chrome.storage family exists precisely to replace it.

What Never Belongs in chrome.storage.sync

This is the section of a chrome storage sync api guide that prevents the worst class of bug.

Never store credentials, API keys, or auth tokens in the sync area. The data lives in the user Google account and is only end-to-end encrypted if the user has set a sync passphrase, which most have not. It travels over TLS and is stored server-side. A token in sync storage is a token in a third-party service you do not control.

Also keep out:

  • Anything over 8 KB per key. It will fail, and it will fail on the device where the data grew rather than the one where you tested.
  • Anything written more than a couple of times a minute. Position tracking, scroll state, live counters. Use session or an in-memory value.
  • Personal browsing data. History, page content, visited URLs. Beyond the storage question, collecting that at all triggers Chrome Web Store data disclosure requirements and turns a trivially reviewable extension into one that needs a privacy policy and a justification.

The cleanest extensions store almost nothing. A single-purpose tool such as a URL copier needs no synced state whatsoever — the Ctrl+Shift+C extension requests clipboard permission only, makes no network calls, and collects no data, because the keyboard binding it depends on is stored by Chrome itself rather than by the extension. Storage is a dependency worth avoiding when the feature does not require it, and the design question worth asking before writing any storage code is whether the state needs to exist at all.

The broader case for narrow permissions is in privacy focused chrome extensions.

Service Workers, State, and Why Storage Is Not Optional

Every chrome storage sync api guide written for Manifest V2 assumed a background page that stayed alive. Manifest V3 replaced persistent background pages with ephemeral service workers. Chrome starts the worker when an event fires and terminates it after a short idle period. Any value held in a module-level variable is gone when that happens.

This is the single most common source of "it works, then it stops working after a few minutes" bug reports. The variable holding your state was fine during testing because you were actively clicking things and the worker never idled.

The pattern that works:

  • Treat the service worker as stateless. Read what you need at the top of each event handler.
  • Use chrome.storage.session for hot state that must survive termination but not a browser restart. It is in memory, so it is fast, and it does not consume sync quota.
  • Use chrome.storage.sync for settings, read on demand rather than cached in a variable.
  • Register all listeners synchronously at the top level of the worker script. Registering a listener inside an async callback means it may not exist when Chrome restarts the worker to deliver an event.

If you are building your first extension and want the surrounding structure this fits into, how to build a chrome extension walks through the manifest and worker layout end to end.

Migrations: Changing Your Schema Without Breaking Users

The step a chrome storage sync api guide usually omits. Once an extension has users, its stored schema is an API. Renaming a key orphans the old value on every installed device.

A workable approach:

  1. Store a schema version number alongside your settings from version one. A single small integer key.
  2. On chrome.runtime.onInstalled with reason update, read the version, run any migrations forward in sequence, then write the new version.
  3. Keep migrations additive where possible. Write the new key, leave the old one for a release or two, then remove it once the population has moved.
  4. Never assume a migration ran. Sync means a device that has been offline for six months can appear with an ancient schema. Read defensively.

Skipping this is fine for an extension with ten users and expensive for one with ten thousand.

Frequently Asked Questions

How much data can chrome.storage.sync hold? Roughly 100 KB in total, with a per-item cap of about 8 KB and a maximum of 512 keys. Size is calculated from the JSON-serialized value plus the key length. Anything larger belongs in chrome.storage.local, which allows around 10 MB and has no per-item limit.

What happens if the user is not signed into Chrome? The API still works. Values are stored locally and behave exactly like local storage. When the user later signs in and enables sync, the stored data is uploaded and begins replicating to their other devices.

Is data in chrome.storage.sync encrypted? It travels over TLS and is stored in the user Google account. End-to-end encryption applies only when the user has configured a sync passphrase, which most have not. Never store credentials, API keys, or auth tokens there.

What are the write rate limits? About 1,800 write operations per hour and 120 per minute. Exceeding either causes writes to reject with a quota error. Debouncing settings writes by around 500 milliseconds keeps normal usage far below both ceilings.

How does Chrome resolve sync conflicts between devices? Last write wins, evaluated per key, with no merging and no conflict event. Structuring settings as many small keys instead of one large object limits the damage a conflict can do and makes writes cheaper.

Does chrome.storage.sync work in Manifest V3 service workers? Yes, and it is the recommended way to hold state, because the service worker is terminated when idle and any module-level variable disappears with it. Read from storage at the start of each event handler rather than caching in memory.

Can content scripts read chrome.storage.sync? Yes. Content scripts can call the storage API directly once the extension declares the storage permission, with no message passing to the service worker required. The onChanged listener fires in content scripts too.

Store Less, Sync Only What Follows the User

The short version of this chrome storage sync api guide: use sync for small preferences that should follow a person across devices, use local for anything large or machine-specific, use session for state that only needs to outlive a service worker, and use none of them for secrets. Split settings into small keys so conflicts stay contained, debounce writes so the rate limit never bites, and treat the service worker as stateless. The best-behaved extensions store the least — Ctrl+Shift+C copies the current tab URL in one keystroke with clipboard permission only, no network calls, and zero data collection. Install it as a reference point for how small a useful extension can be, then keep your own storage layer just as narrow. For the tooling around extension development, see chrome extensions for web developers 2026.

Try Ctrl+Shift+C

Copy any URL with one keyboard shortcut. Free forever, no data collected.