The storage Event and Limits
The storage event
Section titled “The storage event”When a page changes localStorage, the browser fires a storage event in every other tab and window open on the same origin — but not in the tab that made the change. This asymmetry surprises most developers the first time.
// Register this listener in Tab Bwindow.addEventListener('storage', (event) => { console.log('key changed:', event.key); console.log('old value:', event.oldValue); console.log('new value:', event.newValue); console.log('storage area:', event.storageArea); // localStorage object});
// In Tab A, write to localStorage — Tab B's listener fires; Tab A's does NOTThe StorageEvent properties are:
| Property | Type | Description |
|---|---|---|
key | string | null | The key that changed. null when clear() is called. |
oldValue | string | null | The previous value, or null if the key was newly created. |
newValue | string | null | The new value, or null if the key was removed. |
url | string | The URL of the document that made the change. |
storageArea | Storage | null | The localStorage object (never sessionStorage — that event only fires within the same tab). |
The storage event fires only for localStorage. sessionStorage changes do not propagate across tabs because each tab has its own isolated session store.
Cross-tab signalling pattern
Section titled “Cross-tab signalling pattern”sequenceDiagram
participant A as Tab A (writer)
participant LS as localStorage
participant B as Tab B (listener)
A->>LS: setItem("demo:signal", "ping")
LS-->>B: storage event fires
Note over B: key="demo:signal"<br/>newValue="ping"
Note over A: No event fires in Tab A A common use case is broadcasting a logout across tabs:
// In any tab: signal logoutlocalStorage.setItem('demo:auth-event', 'logout:' + Date.now());
// In every other tab: react to itwindow.addEventListener('storage', (e) => { if (e.key === 'demo:auth-event' && e.newValue?.startsWith('logout:')) { // redirect to login page window.location.href = '/login'; }});The ~5 MB quota
Section titled “The ~5 MB quota”The Web Storage specification does not mandate a specific quota size, but browsers universally implement approximately 5 MB per origin for localStorage (some give 10 MB; mobile browsers may give less).
When you exceed the quota, setItem throws a QuotaExceededError (a DOMException):
try { const big = 'x'.repeat(6 * 1024 * 1024); // 6 MB string localStorage.setItem('demo:big', big);} catch (e) { if (e instanceof DOMException && e.name === 'QuotaExceededError') { console.error('Storage quota exceeded — cannot save data'); }}Always wrap writes that could be large (e.g., serialised arrays or objects that grow over time) in a try/catch to handle quota errors gracefully.
The synchronous-blocking caveat
Section titled “The synchronous-blocking caveat”All Web Storage operations — setItem, getItem, removeItem, clear — run synchronously on the main thread. This is normally fine for small values, but it becomes a problem when:
- You are storing large strings (hundreds of KB or more) — each call blocks the main thread while serialising and writing.
- You are calling
setItemin a tight loop — the cumulative blocking adds up. - You are reading a large value inside a
requestAnimationFrameor event handler — frame drops become visible.
If you find yourself storing large blobs, consider IndexedDB (asynchronous, supports binary data) or the Cache API instead.
Runnable: storage event listener + write
Section titled “Runnable: storage event listener + write”The snippet below registers a storage listener and then writes to localStorage. Because the listener only fires in other tabs, you will see the write confirmation but not the event in this window. Open the same page in a second tab to observe the cross-tab event.
Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
storage event | No external library needed — a pure browser API for cross-tab signaling | Only fires for localStorage, and never in the tab that wrote — you must design around this asymmetry |
| Web Storage’s ~5 MB quota | Small enough for the browser to process synchronously and fast, with no extra setup | Once data outgrows 5 MB you must migrate the whole system to IndexedDB, changing the API and mental model entirely |
| A synchronous API | Simple to write — no Promises or callbacks to manage | A large value blocks the main thread directly, freezing the UI during the read/write |
Common mistakes
Section titled “Common mistakes”- Calling
setItemwithout wrapping it intry/catch— when quota is full,setItemthrowsQuotaExceededError, and an app that doesn’t catch it crashes immediately. - Expecting the
storageevent to fire in the tab that wrote — this event only fires in other tabs. To update the UI in the writing tab too, call that logic directly, separate from the listener. - Writing to
localStorageon every keystroke — because every operation is synchronous, this blocks the main thread repeatedly until it feels janky. Always debounce before writing.