Skip to content
C Codeloom
Web

Cookies vs localStorage vs sessionStorage

Compare the three main browser storage mechanisms by lifecycle, capacity, security, and use case so you pick the right tool for tokens, preferences, and state.

·4 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • Lifecycle differences between the three storages
  • Capacity and access patterns
  • Security implications of each
  • Where auth tokens should actually live
  • Practical patterns for real apps

Prerequisites

  • Familiar with HTTP

What and Why

Browsers offer three obvious places to store data on the client: cookies, localStorage, and sessionStorage. They look interchangeable in the dev tools, but they behave very differently. Picking the wrong one is the difference between a login that survives a refresh and one that does not, or between a secure session and a hijackable one.

This article compares them on lifecycle, scope, capacity, network behavior, and security.

Mental Model

  • Cookies: small, automatically attached to HTTP requests, server-readable, configurable with HttpOnly, Secure, SameSite.
  • localStorage: large (~5MB), JS-only, persists until cleared.
  • sessionStorage: same API as localStorage, scoped to a single tab and cleared on close.
                Cookies        localStorage   sessionStorage
Sent w/ HTTP    yes            no             no
JS readable     unless HttpOnly yes           yes
Capacity        ~4 KB / cookie  ~5 MB         ~5 MB
Lifetime        cookie expiry   forever       tab close
Scope           domain + path   origin        origin + tab
Use case        auth, CSRF      prefs, cache  draft form state
Storage at a glance

The single biggest practical difference: cookies travel with every matching request automatically. The other two do not.

Hands-on Example

Setting a secure auth cookie from the server:

Set-Cookie: sid=abc123; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=2592000
  • HttpOnly blocks JS access, neutralizing most XSS token theft.
  • Secure prevents transmission over plain HTTP.
  • SameSite=Lax blocks the cookie on cross-site POSTs, mitigating CSRF.

Reading and writing localStorage:

localStorage.setItem('theme', 'dark');
const theme = localStorage.getItem('theme');
localStorage.removeItem('theme');

sessionStorage works identically, but each tab has its own copy:

sessionStorage.setItem('draft', JSON.stringify(formState));
// gone when the tab closes

A typical division of labor in a modern app:

  • Session ID or refresh token: cookie, HttpOnly + Secure + SameSite=Lax.
  • User preferences (theme, locale): localStorage.
  • In-progress form, wizard step: sessionStorage.
  • Cached API responses: localStorage or IndexedDB depending on size.

If you must use a token in JS (for example, sending it as a header), keep it in memory (a JS variable or a React context). That is wiped on refresh but is not exposed to XSS the way localStorage is.

Common Pitfalls

Storing JWTs in localStorage and calling it secure. Any XSS on your origin can read it. HttpOnly cookies are far safer for session-like tokens.

Setting cookies without SameSite. Older browsers default to None, which means CSRF in many setups. Always set it explicitly.

Assuming sessionStorage is cross-tab. It is not. Two tabs of the same site have separate sessionStorage. Use localStorage plus the storage event for cross-tab sync.

Hitting capacity in cookies. Each cookie is around 4 KB, and the total sent per request adds up. Bloated cookies slow every API call.

Storing PII in localStorage on shared devices. It persists until the user explicitly clears it. Browser sign-out does not touch it.

Mixing storage with iframes. Third-party iframes may have storage blocked (Safari, Firefox strict mode). Do not rely on it for critical flows in embedded contexts.

Practical Tips

  • Default to HttpOnly cookies for anything that authenticates the user. Use CSRF tokens or SameSite for protection.
  • Keep localStorage for non-sensitive UX state. Treat anything you put there as public.
  • Use sessionStorage for per-tab drafts so opening a second tab does not stomp on the first.
  • Wrap storage in a small utility module so you can swap implementations (e.g., move to IndexedDB) without touching every call site.
  • Always JSON-encode and decode explicitly. Storing objects via toString() is a silent footgun.
  • Handle quota errors. setItem throws when storage is full; private browsing modes sometimes have a 0-byte quota.
  • Listen for the storage event if you want preferences to sync between open tabs.

Wrap-up

Three storages, three jobs. Cookies are for credentials and anything the server needs every request. localStorage is for persistent client state that is safe to expose to JS. sessionStorage is for ephemeral, per-tab state.

Pick deliberately. The wrong choice usually shows up as either a security incident or a confusing UX bug, and both are much harder to fix later than to design correctly now.