Chrome Extension Storage API: Choosing Between sync, local, and session Storage

The Chrome extension storage API provides three distinct storage areas: chrome.storage.sync, chrome.storage.local, and chrome.storage.session. Each has different persistence behavior, capacity limits, sync behavior, and appropriate use cases. Picking the wrong one results in either data loss when users switch browsers, unexpectedly stale data, hitting quota limits, or slower-than-necessary data access. The right choice requires understanding the constraints on each.

The Three Storage Areas

chrome.storage.sync

chrome.storage.sync stores data linked to the user’s Chrome Account and syncs it across all of the user’s Chrome installations where they’re signed in. The user changes a setting on their work computer; it’s available on their home computer.

Limits:

  • 100KB total storage per extension
  • 8KB per item (each value in the stored object)
  • 512 items maximum
  • Write operations are rate-limited: 1800 writes per hour maximum
await chrome.storage.sync.set({ theme: 'dark', blocklist: ['ads.example.com'] });
const { theme } = await chrome.storage.sync.get('theme');

Use for: User preferences and settings that should persist across devices. Theme choices, filter lists, configuration options. Not appropriate for large datasets or high-frequency writes.

The sync caveat: If the user isn’t signed into Chrome, chrome.storage.sync falls back to local behavior — data is stored locally but not synced. The API doesn’t inform you of this; writes succeed silently. This is usually fine for settings data but matters if the cross-device sync is a feature the user expects.

chrome.storage.local

chrome.storage.local stores data locally in the extension’s profile directory. No sync, no cross-device persistence. Data persists across browser restarts.

Limits:

  • Default: 10MB total storage
  • With unlimitedStorage permission declared in manifest: no enforced limit (subject to disk space)
await chrome.storage.local.set({ cachedData: largeDataSet, lastFetch: Date.now() });
const { cachedData } = await chrome.storage.local.get('cachedData');

Use for: Cached API responses, large datasets, extension state that doesn’t need cross-device sync, high-frequency-write data that would hit sync’s rate limits. Any storage over 100KB belongs here.

chrome.storage.session

chrome.storage.session was added in Chrome 102. Data persists only for the current browser session — when Chrome closes (or the extension is reloaded), session storage is cleared.

Limits:

  • 1MB total storage by default
  • Can be increased to 10MB with the storage permission
await chrome.storage.session.set({ activeUserId: userId });
const { activeUserId } = await chrome.storage.session.get('activeUserId');

Use for: Temporary state that should not persist across browser restarts. Authentication tokens with short lifespans, in-progress form state, temporary computation results. Also appropriate for data that would otherwise be stored in memory variables in the service worker but gets lost when the service worker goes idle and restarts.

The last point is important: MV3 service workers stop when idle and restart on demand. Any in-memory state is lost on stop. chrome.storage.session provides a memory-like storage area that persists across service worker restarts within a browser session, without the overhead of full local persistence.

Practical Storage Architecture

A typical extension storage architecture:

chrome.storage.sync   → user preferences, settings
chrome.storage.local  → cached data, large datasets, state history
chrome.storage.session → ephemeral auth state, temporary computation state
                         (replaces in-memory service worker variables)

Reading and Writing: Patterns That Work

Type-Safe Reads

chrome.storage.*.get() accepts a key, array of keys, or null (for all keys). It returns an object where missing keys are simply absent:

const result = await chrome.storage.local.get(['setting1', 'setting2']);
const setting1 = result.setting1 ?? defaultValue1;
const setting2 = result.setting2 ?? defaultValue2;

Default values can be passed directly in the get() call:

const { setting1 = defaultValue1, setting2 = defaultValue2 } 
  = await chrome.storage.local.get({ setting1: defaultValue1, setting2: defaultValue2 });

Avoiding Race Conditions on Write

Multiple contexts (popup, content scripts, service worker) may write to storage concurrently. For counters or arrays where the write depends on the current value, read-modify-write patterns have race conditions:

// UNSAFE: two concurrent contexts may both read 5, both write 6
const { count } = await chrome.storage.local.get('count');
await chrome.storage.local.set({ count: count + 1 });

There’s no built-in atomic increment. Mitigations:

  • Funnel writes through a single context (the service worker via messages)
  • Use a locking mechanism (acquire a flag in storage before writing, release after)
  • Use append-only arrays with deduplication on read

For most extension use cases, concurrent writes to the same key are rare enough that this isn’t a practical problem. Design toward single-writer patterns when possible.

Storing Objects vs. Flat Keys

You can store a nested object as a single value:

await chrome.storage.local.set({ settings: { theme: 'dark', fontSize: 16 } });

But updating a nested property requires reading the whole object first:

const { settings } = await chrome.storage.local.get('settings');
settings.theme = 'light';
await chrome.storage.local.set({ settings });

Alternatively, store flat keys:

await chrome.storage.local.set({ 'settings.theme': 'light' });

Flat keys avoid the read-before-write for individual updates at the cost of multiple storage entries. The tradeoff depends on how many settings you have and how often individual settings change.

Change Listeners

All three storage areas fire chrome.storage.onChanged when data changes. The event includes the old and new values:

chrome.storage.onChanged.addListener((changes, areaName) => {
  for (const [key, { oldValue, newValue }] of Object.entries(changes)) {
    console.log(`${areaName}.${key} changed from`, oldValue, 'to', newValue);
  }
});

This fires in all contexts that have the listener — the background worker, popup, and content scripts all receive the event. Use it to react to settings changes without polling.

Quota Management

Check usage programmatically:

const bytesUsed = await chrome.storage.local.getBytesInUse(null);
console.log(`Using ${bytesUsed} of ${chrome.storage.local.QUOTA_BYTES} bytes`);

For sync storage, also check per-item usage since the 8KB per-item limit can be hit before the total limit:

const itemBytes = await chrome.storage.sync.getBytesInUse('largeKey');
if (itemBytes > 7000) {
  // Approaching 8KB item limit — split or compress
}

If you’re approaching sync limits with settings data: consider which settings genuinely need cross-device sync and move others to local. Configuration serialized as JSON can often be compressed before storage if limits are a constraint, though the structured clone approach limits what third-party libraries can do in an extension context.

Privacy Considerations

chrome.storage.sync data is uploaded to Google’s servers associated with the user’s Chrome Account. This is relevant if your extension handles any sensitive user data — filter lists, browsing behavior, or configuration that reveals user interests. For privacy-sensitive extensions, prefer chrome.storage.local and inform users that their data doesn’t leave the device.

The Chrome extension privacy documentation specifies disclosure requirements for data collection, including what counts as “user data” for the Web Store review.

FAQ

Does chrome.storage.local survive extension updates? Yes. Extension updates do not clear local or sync storage. The data persists. Use the onInstalled event with details.reason === 'update' to run migration logic if your storage schema changed.

What happens to sync storage if the user is offline? Writes are queued locally and synced when connectivity is restored. Reads return the locally cached copy of synced data. Chrome handles the sync conflict resolution.

Can I use IndexedDB instead of chrome.storage? Yes. For large structured datasets where query performance matters, IndexedDB is more appropriate than chrome.storage.local. IndexedDB doesn’t have the 10MB default limit (it uses the origin’s full quota allocation) and supports transactions and indexes. chrome.storage.local is simpler for key-value access patterns.

How do I migrate data between storage areas? On extension update, read from the old area, write to the new area, then clear the old area. Use onInstalled with reason === 'update' to trigger the migration, and check a migration flag in storage to ensure it only runs once.