Cookies vs localStorage vs sessionStorage — when to use each?
Cookies are sent on every request and can be HttpOnly/Secure — right for auth tokens. localStorage is JS-readable and persists indefinitely — right for non-sensitive prefs. sessionStorage is JS-readable and dies with the tab — right for ephemeral wizard/draft state.
All three are key-value string stores in the browser, but they differ on three axes that interviewers care about: lifetime, scope, and how they reach the server. Picking the wrong one is how teams ship XSS-exploitable auth or data that mysteriously vanishes.
Cookies. Up to 4KB per cookie. Sent with every HTTP request to the matching domain (this is both the feature and the cost — wasted bandwidth on every static asset). Configurable via attributes: Domain, Path, Expires/Max-Age, Secure (HTTPS only), HttpOnly (JS cannot read — critical for auth), and SameSite (Lax default, Strict for high-security, None for cross-site with Secure). The only storage option for session/auth tokens that the server reads automatically.
localStorage. ~5–10MB per origin. No expiration — persists until the user (or your code) clears it. JS-readable on the same origin. Synchronous API (getItem/setItem/removeItem) — be careful, large reads block the main thread. Survives tab close, browser restart, and OS reboot.
sessionStorage. Same API and quota as localStorage, but scoped to the tab. A new tab gets a fresh store, even on the same origin. Cleared when the tab closes. Right for "draft form contents while the user is filling it out" or "wizard step state."
Auth: cookies, not localStorage. This is the trap interviewers love. localStorage.setItem("token", jwt) exposes the token to any XSS payload — a single <img onerror> injection exfiltrates the session. HttpOnly cookies are unreadable from JS, so XSS can act as the user but cannot steal a long-lived token. Pair with Secure + SameSite=Lax (or Strict if you don't need cross-site nav-then-POST). For SPAs hitting an API on a different origin, you need SameSite=None; Secure + CORS credentials: 'include' + an explicit allowed origin.
CSRF concern. Auto-sent cookies enable CSRF — a malicious site triggers a request that carries the user's cookie. Defenses: SameSite=Lax (modern default, blocks most cross-site POSTs), CSRF tokens for state-changing routes, double-submit cookie pattern.
Storage events & cross-tab sync. window.addEventListener('storage', ...) fires in other tabs (not the writing tab) when localStorage changes — useful for "logout in one tab logs out all tabs." sessionStorage does not fire across tabs (it's tab-scoped).
Quota. 5MB is a floor, not a guarantee. Quota errors throw on setItem — wrap in try/catch and have a fallback (LRU eviction or IndexedDB for larger payloads).
When to reach for IndexedDB instead. Anything larger than a few hundred KB, structured data, or async access — IndexedDB. Storage APIs like navigator.storage.estimate() give you remaining quota.
Quick decision matrix:
- Auth tokens / session → HttpOnly Secure cookie (server-set, server-read).
- User preferences (theme, layout) → localStorage (small, persistent, JS reads).
- Multi-step form draft → sessionStorage (tab-scoped, auto-cleared).
- Cached API responses (large, structured) → IndexedDB (or the Cache API for HTTP responses).
Code
Follow-up questions
- •Why is HttpOnly important even on a site with no XSS today?
- •How does SameSite=Lax differ from Strict?
- •When would you reach for IndexedDB over localStorage?
- •How do you handle quota-exceeded errors?
Common mistakes
- •Storing JWTs in localStorage 'because it's easier' — XSS exfiltration risk.
- •Assuming sessionStorage syncs across tabs (it doesn't).
- •Not catching QuotaExceededError on setItem.
- •Using cookies for large payloads — every request pays the cost.
Performance considerations
- •localStorage is synchronous — large reads block the main thread; offload to IndexedDB.
- •Cookies inflate every request header; keep them small or scope by Path/Domain.
Edge cases
- •Private/incognito mode may give a 0-byte quota or wipe storage on close.
- •Storage event does NOT fire in the same tab that wrote — use a custom event for intra-tab sync.
- •Cookies set with Domain=.example.com leak to all subdomains.
Real-world examples
- •Banking apps: HttpOnly Secure SameSite=Strict cookies for session.
- •Notion: IndexedDB for offline doc cache, localStorage for theme/UI prefs.