← Blog

chrome.storage.local vs sync vs session vs IndexedDB: which to use

Extensions have four persistence options, each with different quotas, lifetimes, and sync behavior. Here's a decision guide — what each one is actually for, the quota limits that bite, and when to reach for IndexedDB instead.

Every extension needs to persist something — settings, tokens, cached data, user content. The platform gives you four options, and picking the wrong one shows up later as a quota error, data that silently doesn’t sync, or a rewrite when your data outgrows where you put it.

Here’s what each is actually for.

The four options at a glance

OptionPersists acrossSynced across devicesRough quotaShape
chrome.storage.localbrowser restartsNo~10 MB (raised from 5 MB)key/value
chrome.storage.syncbrowser restartsYes~100 KB total, ~8 KB/itemkey/value
chrome.storage.sessionuntil browser closesNo~10 MBkey/value
IndexedDBbrowser restartsNolarge (disk-based, %-of-disk)structured DB

Everything below is the detail behind that table.

chrome.storage.local — the default

This is where most extension state belongs. It’s key/value, asynchronous, persists across browser restarts, and is available from every context (service worker, content scripts, popup, options). The quota was historically 5 MB and is now around 10 MB for unpacked/most extensions — generous for settings and modest caches, but not where you put large or unbounded data.

Reach for it by default. Only move off it when you have a specific reason below.

await chrome.storage.local.set({ theme: "dark" });
const { theme } = await chrome.storage.local.get("theme");

chrome.storage.sync — small, cross-device, and quota-tight

sync looks like local but Chrome replicates it across the user’s signed-in devices. That’s genuinely useful for user settings — a handful of small values the user expects to follow them.

The trap is the quota. It is small and it is enforced hard:

If you treat sync like local and dump a cache or a growing list into it, you will hit QUOTA_BYTES_PER_ITEM or the write-rate limit, and the failure often surfaces far from the code that caused it. Sync is for preferences, not data.

It also introduces a concern local doesn’t have: conflicts. Two devices can write the same key while offline and reconcile later. If that matters for your data, you need to handle the onChanged events and decide a merge strategy — last-write-wins is what you get by default, and it silently drops one side.

chrome.storage.session — in-memory, cleared on close

session holds data only for the life of the browser session — it’s cleared when the browser closes. Its reason to exist in MV3 is specific: because the service worker is ephemeral and loses its in-memory variables on every eviction, you need somewhere to keep short-lived runtime state that survives worker restarts but shouldn’t be written to disk.

Auth tokens for the current session, decrypted secrets, “is the user mid-flow” flags — things you want to outlive a worker eviction but not persist to disk or leak across sessions. By default session is not exposed to content scripts; you can opt in with setAccessLevel if you need it there.

IndexedDB — when key/value isn’t enough

All three chrome.storage areas are key/value stores. The moment you need any of the following, you’ve outgrown them:

IndexedDB is the browser’s real database: transactional, indexed, disk-backed, with a quota measured as a percentage of available disk rather than a fixed small cap. It’s available in the service worker and in content scripts (via the page, with caveats). The cost is ergonomics — the raw IndexedDB API is famously awkward, which is why most teams wrap it.

A decision guide

The migration you don’t want to do later

The most expensive mistake here is starting with chrome.storage.local for something that was always going to be large or queryable, then rewriting to IndexedDB once you hit the wall — and having to migrate every existing user’s data in place, in a service worker that can die mid-migration. If you can see that a data set will grow unbounded or need querying, start it on IndexedDB.

And whatever you choose, remember that schema changes are forever: users upgrade your extension in place, carrying their old data shape with them. Versioned, resumable migrations on runtime.onInstalled aren’t a nice-to-have — they’re the difference between an update that ships and one that corrupts data for the users who had the most of it.


Part of a series mapping every layer of the browser-extension stack. See also the MV3 service worker lifetime, which is why chrome.storage.session exists in the first place.

Building this for real?

Exo is an application framework for browser-native software — the patterns in this post are the ones it paves over. It's in alpha and we're onboarding founding partners.

Join the Alpha More posts