Skip to main content
An extension has four storage areas, and they are not interchangeable. local holds device data, sync carries small preferences between signed-in profiles, session lives in memory for as long as the browser runs, and managed is read-only policy data. This page gives the quota and the reader set for each area, the rule that keeps storage.session away from content scripts, and the console lines that a full or throttled area prints.

Three ways to keep state, and why storage wins

A variable in the service worker. A Manifest V3 background service worker stops when it goes idle and starts again on the next event. Everything that you held in memory is gone, and no error announces it. localStorage or IndexedDB in an extension page. Both are scoped to the page origin. The service worker cannot use localStorage at all, and a content script sees the host page’s storage, not yours. A chrome.storage area. Asynchronous, readable from every extension context that has the storage permission, and unaffected by a worker restart. This is the default answer for extension state.

The four storage areas

Quotas below are Chromium’s defaults. sync also throttles writes: about 120 writes a minute and about 1,800 an hour. A write inside a keystroke handler reaches that ceiling quickly, so debounce and write once. Choose by the question that the data answers:
  • Feature state, caches, and durable settings go in local.
  • Small preferences that a user expects on a second computer go in sync.
  • Per-run coordination that must not outlive the browser goes in session.
  • Enterprise policy values are read from managed, and never mixed with user-editable settings.
chrome.storage is not the only option. IndexedDB and the Origin Private File System (OPFS) work in extension pages and workers for large or structured data, under the browser’s normal quota. In a content script both belong to the host page’s origin, so keep that data in the extension’s own contexts.

Manifest snippet

The storage permission covers all four areas. unlimitedStorage raises the local cap, and managed needs a schema file:
A settings module that restores from storage instead of trusting memory:

Reading storage.session from a content script

storage.session starts closed to untrusted contexts, which is what a content script is. A read from there throws instead of returning an empty object. Open the area from the service worker, once, before the content script asks:
Leave the area closed when the values are sensitive. Route the read through a message to the service worker instead, as Messaging describes.

Per-browser differences

Firefox implements all four areas. Treat the numbers in the quota table as Chromium’s, and check the limit that your feature depends on against a Firefox build before you ship it. If your source calls browser.storage and you also build for a Chromium target, pass --polyfill. See Cross-browser compatibility.

Console lines you will see

Copy the line that you see into search. Each one maps to one cause. QUOTA_BYTES_PER_ITEM quota exceeded One value under one sync key is larger than 8 KB. Split the value across keys, or move that key to local. QUOTA_BYTES quota exceeded The area is full: 100 KB across all keys for sync, the device cap for local. Store less, or add unlimitedStorage for the local case. MAX_WRITE_OPERATIONS_PER_MINUTE quota exceeded Too many sync writes in one minute, usually one write per keystroke or per scroll event. Debounce the handler and write the settled value once. Access to storage is not allowed from this context. A content script read storage.session while the area is still trusted contexts only. Call setAccessLevel from the service worker, or move the read behind a message. A callback-style call reports these through chrome.runtime.lastError, which is silent until you read it. The promise form rejects, so await inside a try block and you see the same text as a caught error.

The Extension.js way

Extension.js does not wrap or replace the storage APIs. It compiles the code that calls them, so the platform rules above are the whole contract. Two build behaviors are worth knowing:
  • The storage.managed_schema path is validated and the schema file is emitted to the output. The path must resolve to a file inside the extension directory. Extension.js flags a path outside it as a load blocker before the browser launches, because Chrome refuses the whole extension over a missing managed schema. See JSON.
  • --polyfill bridges browser.* onto Chromium targets, so one source can call browser.storage for both families.
Because the background service worker restarts, write the state that matters and read it back on demand. Keep the in-memory copy as an optimization, never as the source of truth. Background scripts covers the restart model. Version the shape of your stored data before the first release, and keep a migration path for a renamed key. A stored object outlives the code that wrote it.

See also