chrome.storage.session vs local

Choose between chrome.storage.session and chrome.storage.local in MV3: lifetime, quota, content script access levels, and the tab-scoped state pattern session storage was designed for.

Published July 25, 2026 Updated July 25, 2026 9 min read
Table of Contents

Since the service worker loses its memory on every eviction, extensions push everything into chrome.storage.local — including per-tab scratch data that has no business being written to disk, and that then has to be cleaned up when tabs close. chrome.storage.session exists for exactly that data: in-memory, cleared when the browser closes, and never written to the profile. This page is part of Chrome Storage API & Sync and covers which area a given key belongs in, and the access-level setting that catches people out.

Root cause: three areas, three different lifetimes

The three storage areas differ on exactly two axes — how long the data lives, and who can read it. local survives browser restarts and lives on disk. sync does the same and additionally replicates through the user’s account, with much smaller quotas. session lives in memory only, survives worker eviction but not a browser restart, and by default is not readable by content scripts at all.

What each storage area survivesIn-memory variables are lost on eviction, session storage survives eviction but not restart, and local and sync survive both.Module-scope variablelost on evictionnever a source of truthstorage.sessionsurvives eviction, cleared on browser exitin memory, ~10 MBstorage.localsurvives restart, on disk~10 MB, unlimitedStorage lifts itstorage.syncsurvives restart and follows the account100 KB total, 8 KB per item
Pick the shortest lifetime that still outlives the thing you are protecting against — usually eviction, not restart.

Step 1. Route each key by what would break if it vanished

The decision is mechanical once framed correctly: ask what happens if this key is missing on the next read. If the answer is “recompute it”, session storage is right. If the answer is “the user loses work or settings”, it belongs in local or sync.

Choosing the area for one keyDecision tree routing a key to session, local or sync storage based on whether it can be recomputed and whether it should follow the user across devices.If this value were missing on the next read, what would happen?Recomputed cheaplystorage.sessionper-tab, per-run scratchNo cleanup codecleared on browser exitUser loses workstorage.localdevice-specific settings, cachesPrune it yourselfnothing clears it for youShould follow the userstorage.syncsmall preferences onlyMind the quotas8 KB per item, 100 KB total
Most per-tab state can be recomputed, which is why so much of what sits in local storage today belongs in session.

Step 2. Use session storage for per-tab state

Per-tab scratch state is the case session storage was built for. Keying by tab id in local means writing cleanup code for tabs.onRemoved; in session the whole area disappears with the browser, and stale tab keys cost nothing but a few bytes of memory until then.

 1// background.js — per-tab analysis results, keyed by tab id
 2const tabKey = (tabId) => `tab:${tabId}`;
 3
 4export async function rememberTabState(tabId, state) {
 5  await chrome.storage.session.set({ [tabKey(tabId)]: state });
 6}
 7
 8export async function readTabState(tabId) {
 9  const key = tabKey(tabId);
10  const result = await chrome.storage.session.get(key);
11  return result[key] ?? null;
12}
13
14// Still worth removing on tab close, but now it is an optimisation, not correctness.
15chrome.tabs.onRemoved.addListener((tabId) => {
16  void chrome.storage.session.remove(tabKey(tabId));
17});

Execution context: Extension service worker. chrome.storage.session requires no separate permission beyond "storage", and reads are faster than local because nothing touches disk. The onRemoved cleanup is now optional rather than load-bearing — if the worker is evicted before it runs, the stale key is harmless and will be gone at the next browser restart. Compare this with the tab lifecycle events you would otherwise have to handle exhaustively.

Step 3. Decide whether content scripts may read it

By default storage.session is restricted to trusted contexts — the worker and your extension pages — and a content script reading it gets nothing. That default is a security feature: content scripts share a process with the page. If a content script genuinely needs the data, either raise the access level deliberately or, better, keep the data in the worker and answer requests over messaging.

1// background.js — only if content scripts truly need direct reads
2await chrome.storage.session.setAccessLevel({
3  accessLevel: "TRUSTED_AND_UNTRUSTED_CONTEXTS",
4});

Execution context: Extension service worker, typically called once from onInstalled. The setting is extension-wide and persists, so it is not a per-key decision — raising it exposes every session key to every content script you inject, on every site you have access to. The default TRUSTED_CONTEXTS is the right answer for almost all extensions; chrome.storage.local has no equivalent restriction, which is one more reason not to treat the two areas as interchangeable.

Step 4. Size the areas against what you actually store

The quotas are far enough apart that they decide some questions on their own. A per-tab record of a few hundred bytes fits comfortably in session storage for hundreds of tabs; the same record synced would exhaust the sync budget within a normal browsing session.

Capacity of each area against a realistic payloadBar chart comparing the sync per-item and total quotas, the session and local ceilings, and the size of a typical per-tab record.One per-tab record0.4 KBtypical scratch payloadsync — per item8 KBhard per-key limitsync — total100 KBacross every synced keysession — total10240 KBin memorylocal — total10240 KBon disk
A 400-byte per-tab record is invisible against the session ceiling and impossible against the sync one.

There is a second, less obvious consequence. Because session storage never touches the disk, writing to it does not contribute to the write amplification that makes frequent local writes expensive on a laptop — so a hot path that updates state on every tabs.onUpdated event is a genuinely better citizen in session storage, quite apart from the cleanup it saves.

Step 5. Move an existing key from local to session

Most extensions arrive at this page with per-tab data already sitting in local. Moving it is a two-release job rather than a single rewrite, because the old keys are on real users’ disks and nothing else will ever clean them up.

 1// background.js — release N: write to session, read from both, prune local in the background
 2const tabKeyLocal = (tabId) => `tabState:${tabId}`;
 3
 4export async function readTabStateCompat(tabId) {
 5  const sessionKey = tabKey(tabId);
 6  const fromSession = (await chrome.storage.session.get(sessionKey))[sessionKey];
 7  if (fromSession) return fromSession;
 8
 9  // Fall back once to the old location, then promote the value and drop the old copy.
10  const legacyKey = tabKeyLocal(tabId);
11  const fromLocal = (await chrome.storage.local.get(legacyKey))[legacyKey];
12  if (!fromLocal) return null;
13
14  await chrome.storage.session.set({ [sessionKey]: fromLocal });
15  await chrome.storage.local.remove(legacyKey);
16  return fromLocal;
17}

Execution context: Extension service worker. Reading through both areas for one release means no user loses state at the moment of the update, and the promote-then-remove ordering keeps an interrupted read from dropping the value. A background sweep is worth adding alongside it, because tabs that are never visited again will otherwise leave their old local keys behind indefinitely: enumerate them once from onStartup with chrome.storage.local.get(null), remove anything matching the legacy prefix, and delete the compatibility path in the release after that.

Cross-browser variation

  • Chrome 120+: storage.session has a roughly 10 MB quota (1 MB before Chrome 112) and defaults to trusted contexts only. setAccessLevel is available from Chrome 102.
  • Firefox 121+: browser.storage.session is supported with the same semantics. Because the background context is an event page, code that leaned on module-scope state works there and fails in Chrome — session storage is the portable fix.
  • Safari 17+: supports storage.session from Safari 16.4. Treat the quota as unspecified and keep session payloads small.
  • All engines: the area is cleared when the browser exits, and it is not part of the synced profile. Never migrate data into it, and never assume it survives a restart.

Verification

  1. Write a key to session, stop the service worker from the extensions page, then trigger an event and read it back. It must still be there — that is the eviction case.
  2. Quit and reopen the browser, then read the same key. It must be gone.
  3. From a content script, read a session key with the default access level. Expect an empty result, confirming the trusted-context restriction.
  4. Compare chrome.storage.session.get and chrome.storage.local.get timings for the same payload in the worker console; the session read should be consistently faster.

FAQ

Does storage.session survive a service worker eviction?

Yes — that is its purpose. It lives in the browser process, not the worker, so it outlives every eviction within a browser session while still costing nothing on disk.

Is session storage faster than local?

Measurably, because there is no disk involved. The difference is small in absolute terms for a single small key, but it matters on a cold start where several reads sit on the critical path.

What happens to session storage during an extension update?

An update discards the worker and installs new code, and session storage is cleared with it — treat an update like a browser restart for this area. Anything a migration needs to read must therefore already be in local or sync before the update lands, which is another reason migrations never target session.

Does a private browsing window share the same session storage?

No. An extension enabled in a private window runs in a separate context with its own in-memory area, so a key written in a normal window is not visible there and vice versa. Code that assumes one shared area per browser session will read empty values in private windows and should fall back to defaults rather than treating the absence as an error.

Can I use session storage for a cache of fetched data?

Only if losing it on browser exit is acceptable. A cache that should survive a restart belongs in local; a cache that only needs to survive eviction — which is most of them, within one browsing session — is a good fit for session.

Why can my content script not read session storage?

The default access level is trusted contexts only. Either keep the data in the worker and serve it over messaging, or call setAccessLevel and accept that every session key becomes readable from every content script you inject.

Other Core APIs & Cross-Browser Data Management Resources