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.