chrome.storage API Tutorial: Save Extension Data
How to persist settings and user data in a Chrome extension with chrome.storage — including sync vs local and quota limits.

Extensions cannot rely on localStorage inside a service worker, so chrome.storage is the standard way to persist data. It is asynchronous, available in every extension surface, and shared across the popup, options page, content scripts and background worker.
Which area should you use?
- chrome.storage.sync — small user settings that follow the signed-in profile. ~100KB total, 8KB per item.
- chrome.storage.local — bigger local-only data, roughly 10MB (more with unlimitedStorage).
- chrome.storage.session — in-memory data cleared when the browser closes; good for tokens.
Reading and writing
Add "storage" to permissions, then: await chrome.storage.sync.set({ theme: "dark" }) and const { theme } = await chrome.storage.sync.get({ theme: "light" }). Passing an object to get gives you defaults for free.
Reacting to changes
chrome.storage.onChanged.addListener((changes, area) => ...) fires in every surface, which is the cleanest way to keep a popup and a content script in sync without custom messaging.
When to use something else
For thousands of records, full-text search or blobs, use IndexedDB in the service worker. Keep chrome.storage for settings and small state.
The exact quota numbers
- chrome.storage.sync: 102,400 bytes total, 8,192 bytes per item, and a maximum of 512 items.
- chrome.storage.sync also caps writes at 120 operations per minute and 1,800 per hour — batch updates instead of writing on every keystroke.
- chrome.storage.local: about 10MB by default; request the "unlimitedStorage" permission to remove the cap.
- chrome.storage.session: memory-only, no persisted quota beyond the local limit, and cleared automatically when the browser process ends.
Batching writes to avoid MAX_WRITE_OPERATIONS_PER_MINUTE
If you get the error 'chrome.storage.sync.set() MAX_WRITE_OPERATIONS_PER_MINUTE quota exceeded', you are calling set() too often — for example on every input event. Debounce writes with setTimeout so you flush once after 300-500ms of inactivity instead of on every keystroke.
Storage.getBytesInUse for quota-aware UI
chrome.storage.sync.getBytesInUse(null) (or a specific key) returns the current byte usage so you can warn users before they hit the 100KB ceiling — useful for extensions that let people save lists or snippets that grow over time.
Migrating from localStorage
- Read every key out of localStorage on first run after upgrade.
- Write the values into chrome.storage.local with chrome.storage.local.set().
- Delete the localStorage keys once the write confirms, and set a migration-complete flag so it only runs once.
- Remember localStorage is unavailable inside a Manifest V3 service worker at all, so this migration has to run from a content script or the popup.
chrome.storage vs cookies for cross-device sync
chrome.storage.sync automatically syncs through the user's signed-in Chrome profile with no server of your own required, which is why ManifestGo defaults generated extensions to chrome.storage.sync for settings and only falls back to chrome.storage.local for larger cached data.
Frequently asked questions
Is chrome.storage encrypted?
No. It is stored on disk in the profile directory. Never keep secrets you would not want a local user to read; move real credentials to a backend.
Why is my chrome.storage.sync write failing?
You likely exceeded the 8KB per-item or 100KB total quota, or the write-per-minute limit. Switch that key to chrome.storage.local.
Does chrome.storage.sync work when the user is offline?
Yes, reads and writes still work locally offline; Chrome queues the sync to other devices and pushes it once connectivity returns.
Can content scripts use chrome.storage directly?
Yes, as long as the extension has declared the "storage" permission — content scripts share the same storage API access as the background service worker and popup.
What happens if I exceed chrome.storage.local's 10MB limit?
chrome.runtime.lastError is set with a quota-exceeded message and the set() call fails silently unless you check lastError, so always check it or request the unlimitedStorage permission for storage-heavy extensions.