> ## Documentation Index
> Fetch the complete documentation index at: https://extension.js.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Extension storage: local, sync, and managed

> Choose between chrome.storage.local, sync, session, and managed. Quotas, what survives an update or an uninstall, which contexts can read each area, and the quota exceeded console errors.

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.

| Where the state lives | Survives a service worker restart | Readable from another context | API shape |
| - | - | - | - |
| Service worker variable | No | No | Synchronous |
| Extension page `localStorage` | Not applicable, page scoped | No | Synchronous |
| `chrome.storage` area | Yes, except `session` on restart | Yes | Promise or callback |

## The four storage areas

Quotas below are Chromium's defaults.

| Area | Quota | Survives a browser restart | Survives an update or an uninstall | Which contexts read it |
| - | - | - | - | - |
| `local` | About 10 MB, and `unlimitedStorage` lifts the cap | Yes | Survives an update, removed on uninstall | Every extension context, content scripts included |
| `sync` | 100 KB total, 8 KB per item, 512 items | Yes, and it follows the profile | Survives an update, removed on uninstall | Every extension context, content scripts included |
| `session` | About 10 MB, held in memory | No, it clears when the browser closes | Cleared when the extension reloads or updates | Trusted contexts only, until `setAccessLevel` opens it |
| `managed` | Set by policy, your code cannot write to it | Yes, the policy supplies it | Comes from the policy, not from your data | Every extension context, read only |

`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:

```json theme={null}
{
  "manifest_version": 3,
  "name": "My Extension",
  "version": "1.0.0",
  "permissions": ["storage", "unlimitedStorage"],
  "storage": {
    "managed_schema": "schema.json"
  },
  "background": { "service_worker": "background.js" }
}
```

A settings module that restores from storage instead of trusting memory:

```ts theme={null}
const SETTINGS_KEY = "settings";

export async function getSettings() {
  const result = await chrome.storage.local.get(SETTINGS_KEY);
  return result[SETTINGS_KEY] ?? { theme: "system" };
}

export async function setSettings(nextSettings: {
  theme: "system" | "light" | "dark";
}) {
  await chrome.storage.local.set({ [SETTINGS_KEY]: nextSettings });
}
```

## 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:

```ts theme={null}
// In the service worker.
chrome.storage.session.setAccessLevel({
  accessLevel: "TRUSTED_AND_UNTRUSTED_CONTEXTS",
});
```

Leave the area closed when the values are sensitive. Route the read through a message to the service worker instead, as [Messaging](/docs/implementation-guide/messaging) describes.

## Per-browser differences

| Capability | Chromium | Firefox | Safari |
| - | - | - | - |
| Namespace | `chrome.storage`, promise or callback | `browser.storage` with promises, and `chrome.storage` is also available | Not covered by these docs |
| `storage.sync` | Signed-in Chrome profile | Needs an add-on id under `browser_specific_settings.gecko.id` | Not covered by these docs |
| `storage.session` | Present, trusted contexts only until `setAccessLevel` runs | Present | Not covered by these docs |
| `storage.managed` | Enterprise policy plus the `storage.managed_schema` key | Enterprise policy or a native manifest, no `managed_schema` key | Not covered by these docs |

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](/docs/features/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](/docs/implementation-guide/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](/docs/implementation-guide/background) 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

* [Messaging](/docs/implementation-guide/messaging)
* [Background scripts / service worker](/docs/implementation-guide/background)
* [JSON](/docs/implementation-guide/json)
* [manifest.json](/docs/implementation-guide/manifest-json)
* [Permissions and host permissions](/docs/implementation-guide/permissions-and-host-permissions)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.