Web Storage APIs: localStorage, sessionStorage, and IndexedDB Explained

Browser storage looks simple from the outside: a few APIs that put data somewhere and let you get it back later. In practice, the choice between localStorage, sessionStorage, and IndexedDB has real consequences for performance, capacity, and reliability — and the wrong choice tends to manifest as bugs at scale rather than during development.

localStorage: Simple, Synchronous, and Slower Than You Think

localStorage is the API most developers reach for first because the surface area is tiny — setItem, getItem, removeItem, clear — and it persists across page loads. The data is string-only, scoped to the origin, and survives until explicitly cleared or until the browser evicts it under storage pressure.

The two things to know about localStorage are that it’s synchronous and that it lives on the main thread. Every read and write blocks the UI. For small bits of state — a feature flag, a preferences object, a session token — this is fine. For anything that grows linearly with usage, localStorage becomes a performance problem long before it becomes a capacity problem. A 200KB localStorage read on a slow device is a measurable jank event.

sessionStorage: Same API, Different Lifetime

sessionStorage shares localStorage’s API exactly but ties data to the tab’s session. Close the tab and the data is gone. Open a new tab to the same origin and you get a fresh, empty store. The use case is narrow: workflow state that should survive a refresh but not a tab close. Multi-step forms, wizard flows, temporary draft state. Don’t use it as a general cache.

IndexedDB: The Real Database

IndexedDB is the only browser storage API designed for non-trivial data. It’s asynchronous, transactional, supports indexes and queries, and handles binary data (Blobs, ArrayBuffers) natively. Capacity is much larger — browsers will typically let an origin use a substantial fraction of available disk before evicting.

The downside is the API. IndexedDB’s native interface is verbose, event-based, and easy to use incorrectly. Most production code wraps it with Dexie, idb-keyval, or a similar library. If you’re shipping a feature that stores anything more complex than a flat key-value map, start with IndexedDB and a wrapper, not localStorage.

Quotas and Eviction

All browser storage shares a per-origin quota. Chrome and Firefox both allow up to about 60% of total disk space per origin in aggregate across all storage APIs, but they reserve the right to evict data when the disk gets full or when the user clears site data. Eviction is generally LRU at the origin level: an origin you haven’t visited in a while can have its entire storage cleared.

What this means in practice: never assume browser storage is permanent. Treat it as a cache with strong locality guarantees, not as authoritative state. Anything that must survive should also live server-side.

Cross-Tab Coordination

localStorage fires a storage event in other tabs when it’s modified — useful for cross-tab coordination of things like login state, settings changes, or shared session data. IndexedDB doesn’t have an equivalent built-in event, but you can use BroadcastChannel for the same pattern with more control.

A common mistake: assuming the storage event fires in the tab that did the write. It doesn’t. Only other tabs hear it.

Security Considerations

All three APIs are origin-scoped, so a script from example.com can’t read data written by evil.com. But within an origin, any script can read any storage — including third-party scripts you’ve embedded and any XSS payload that runs in your page. This makes localStorage a poor choice for sensitive data like long-lived auth tokens. The right place for those is httpOnly cookies, which JavaScript can’t touch.

For related context, see our writeup on browser storage quotas and our deeper guide to the extension Storage API.

Frequently Asked Questions

Is localStorage faster than IndexedDB for small reads?

Often yes for the read itself — localStorage is synchronous and has less overhead — but the synchronous-on-the-main-thread cost is real for anything beyond a few hundred bytes.

Why does IndexedDB feel so much harder to use than localStorage?

Because it’s a real database with transactions and versioned schemas, and the original API was designed around DOM events rather than Promises. Use a wrapper library; the core API is intentionally low-level.

Can I store auth tokens in localStorage?

You can, but you probably shouldn’t. Any XSS in your origin can read localStorage. Cookies with HttpOnly and Secure flags are not readable by JavaScript and are the better default for session tokens.

How much can I actually store?

Per-origin quotas are usually generous — tens of gigabytes on a desktop with plenty of disk — but they’re not guaranteed. Browsers can and do evict origin storage when disk pressure rises.