Skip to main content
Web

localStorage vs sessionStorage vs IndexedDB vs cookies: browser storage without the 3am regret

Four ways to store data in the browser, four very different tradeoffs. A field guide to capacity, persistence, and security so you pick the right one before it becomes an incident.

Thien Nguyen
By Thien Nguyen
Updated July 20, 2026 · 4 min read

The 3am version of this story is always the same shape: something got stored in the wrong place. A cart that vanished because it lived in sessionStorage and the user closed the tab. An auth token sitting in localStorage that an XSS bug quietly exfiltrated. A cookie so large it pushed every single request over a proxy's header-size limit. Four APIs, four very different failure modes — and the browser will happily let you use any of them for anything.

The four options, side by side

CookieslocalStoragesessionStorageIndexedDB
Capacity~4KB~5–10MB~5–10MBHundreds of MB+
PersistenceSet by Expires/Max-Age, or sessionUntil explicitly clearedUntil tab closesUntil explicitly cleared
Sent to server automaticallyYes, on every matching requestNoNoNo
API styleString header, manual parsingSynchronous, key-value stringsSynchronous, key-value stringsAsynchronous, structured objects
Shared across tabsYesYesNo (per-tab)Yes

That "sent to server automatically" row is the one people forget, and it's the source of most of the surprises below.

Cookies: the only one the server sees

Cookies are the odd one out — every cookie that matches a request's domain and path gets attached to that request automatically, which is exactly why they're still the right tool for session identifiers and auth tokens. Pair HttpOnly (blocks JavaScript from reading it, which blunts XSS) with Secure (HTTPS only) and SameSite (controls cross-site sending, your main CSRF defense), and you get a storage mechanism the server can trust without any extra plumbing. The cost is the tiny ~4KB budget and the fact that every cookie rides along on every matching request — stash something large in a cookie and you've just added latency to your entire site.

localStorage: convenient, and that's the problem

localStorage is a synchronous key-value string store that survives tab closes, browser restarts, even reboots — nothing clears it except an explicit .removeItem(), .clear(), or the user wiping site data. That persistence is exactly why it's tempting to drop an auth token in there... and exactly why you shouldn't. Anything in localStorage is readable by any JavaScript running on the page, including a malicious script from a successful XSS injection or a compromised third-party dependency. A cookie with HttpOnly set is invisible to that same script; localStorage has no equivalent flag. Use it for UI preferences, cached non-sensitive API responses, feature-flag state — not credentials.

sessionStorage: same API, opposite lifetime

sessionStorage shares localStorage's synchronous string-only API but clears the moment the tab closes, and — this is the part that catches people — it is not shared across tabs, even two tabs on the exact same origin. Open your app in a second tab and sessionStorage starts empty. It's the right call for data that should genuinely die with the tab: a multi-step form's in-progress state, a one-time redirect target, anything you'd be fine losing if the user just closes the window.

IndexedDB: the one built for real data

IndexedDB is a full asynchronous, transactional, indexed database running in the browser — structured objects (not just strings), hundreds of megabytes of headroom depending on available disk space, and query support via indexes. The tradeoff is the API: it's callback/promise-based and noticeably more verbose than localStorage.setItem(), which is why most teams reach for a wrapper library (Dexie, idb) rather than the raw API. Reach for it when you're storing anything structured or sizeable — offline-first app data, cached datasets, file blobs — where localStorage's 5–10MB string-only ceiling won't cut it.

The decision in one pass

  1. Does the server need to see it on every request? → Cookie.
  2. Is it a credential or anything sensitive? → Cookie with HttpOnly + Secure + SameSite, never localStorage.
  3. Should it die when the tab closes, and only that tab?sessionStorage.
  4. Is it structured, large, or does it need querying? → IndexedDB.
  5. Everything else — small, non-sensitive, should survive a refreshlocalStorage.

Building or debugging cookies

If cookies are the piece you're working with, our Cookie Builder assembles a spec-correct Set-Cookie header from a form — name, value, domain, path, expiry, and all the security flags — so you don't hand-write the semicolon-separated syntax. And when you're staring at a raw Cookie: or Set-Cookie: header from DevTools or a curl -v dump, the Cookie Parser breaks it into a readable table, flags whether Secure/HttpOnly/SameSite are actually set, and decodes any percent-encoded values. Both run entirely client-side.

References

Primary documentation and specifications checked when this article was last updated.

WebBrowser StorageFundamentals

Related articles

All articles