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
Thestorage permission covers all four areas. unlimitedStorage raises the local cap, and managed needs a schema file:
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:
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_schemapath 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. --polyfillbridgesbrowser.*onto Chromium targets, so one source can callbrowser.storagefor both families.

