Browser storage comparison

Cookies vs localStorage

The two main ways browsers store data on the client side. Cookies were designed for server–client communication and carry security attributes that matter in production. localStorage was designed for client-side persistence with no server involvement. Choosing the right mechanism affects security, privacy, and performance — and the vocabulary comes up in almost every frontend and security code review.

TL;DR

  • Cookies — sent to the server automatically on every request, configurable expiry, HttpOnly flag blocks JavaScript access, Secure flag enforces HTTPS, SameSite flag mitigates CSRF. Up to ~4 KB. Best for authentication tokens and session IDs.
  • localStorage — JavaScript-only, never sent to the server, no built-in expiry, up to ~5 MB. Best for user preferences, theme settings, and cached UI data. Do not store sensitive credentials here unless XSS is fully mitigated.
  • sessionStorage — identical to localStorage in API and capacity, but scoped to one browser tab and cleared when the tab is closed. Best for ephemeral per-tab state.

Side-by-side comparison

AspectCookieslocalStorage
Server access Yes — sent automatically via the Cookie header on every same-origin request No — JavaScript only, never transmitted to the server
Capacity ~4 KB per cookie (~50 cookies per domain) ~5–10 MB per origin
Expiry Configurable via Expires or Max-Age; omitting both creates a session cookie (cleared on browser close) None — persists until explicitly cleared by JavaScript or the user
JavaScript access document.cookie (string API) — unless HttpOnly is set localStorage.getItem() / setItem() — always accessible to JS
HttpOnly flag Yes — completely blocks JavaScript read/write access No equivalent — always readable by any script on the page
Secure flag Yes — cookie only sent over HTTPS connections Not applicable (never sent over the network)
SameSite flag Yes — Strict, Lax, or None; primary CSRF defence No equivalent
Domain scope Can be scoped to a parent domain (e.g. Domain=example.com) so all subdomains receive it Strictly origin-scoped — subdomains cannot access each other's data
XSS risk Mitigated with HttpOnly; otherwise token can be read via document.cookie High — any injected script can call localStorage.getItem()
CSRF risk Present if SameSite is not set correctly; SameSite=Strict eliminates it None — never sent automatically by the browser

What is an HTTP cookie?

A cookie is a small piece of data the server sends to the browser via the Set-Cookie response header. The browser stores it and includes it automatically in subsequent requests to the same origin via the Cookie request header. This automatic transmission is what makes cookies essential for session management — the server can identify the user without them re-sending credentials on every request.

Cookies carry optional attributes that control their security and lifetime:

  • HttpOnly — prevents JavaScript from reading the cookie. An attacker who injects malicious JavaScript cannot exfiltrate an HttpOnly cookie. Always set this on authentication cookies.
  • Secure — the cookie is only transmitted over HTTPS. Prevents interception on unencrypted connections.
  • SameSite=Strict — the cookie is never sent on cross-site requests, blocking CSRF attacks entirely. Use Lax if you need the cookie sent on top-level cross-site navigations (e.g. clicking a link from another site).
  • Domain — controls which (sub)domains receive the cookie. Domain=example.com sends the cookie to all subdomains.
  • Path — restricts the cookie to a URL path prefix; Path=/api stops the cookie being sent on asset requests.
  • Expires / Max-Age — sets an absolute or relative expiry. Without either, the cookie is a session cookie and is removed when the browser closes.
# Server sets a secure auth cookie
Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Strict; Max-Age=604800; Path=/

What is localStorage?

localStorage is part of the Web Storage API (alongside sessionStorage). It is a simple key-value store accessible via JavaScript on the same origin. Data is persisted across browser sessions — it survives page refreshes, tab closes, and even browser restarts — until explicitly removed.

Unlike cookies, localStorage is never sent to the server. This makes it suitable for large client-side data such as cached JSON responses, user preference objects, or offline application state, but unsuitable for anything the server needs to read (such as session identifiers or CSRF tokens).

The API is synchronous — reading or writing large objects blocks the main thread — so prefer IndexedDB for large or structured datasets.

// Storing and reading a preference
localStorage.setItem('theme', 'dark');
const theme = localStorage.getItem('theme'); // 'dark'

// Objects must be serialised to JSON — localStorage only stores strings
const prefs = { fontSize: 16, sidebar: true };
localStorage.setItem('prefs', JSON.stringify(prefs));
const loaded = JSON.parse(localStorage.getItem('prefs') ?? '{}');

// Clearing a single key or all data
localStorage.removeItem('theme');
localStorage.clear();

How engineers talk about cookies vs localStorage

Cookie conversations

  • "Set the session cookie as HttpOnly so JavaScript can't touch it — otherwise we're wide open to token theft via XSS."
  • "Use SameSite=Strict to prevent the cookie being sent on cross-site requests — that kills most CSRF vectors."
  • "The cookie is scoped to /api — it won't be sent for static asset requests, which keeps the headers clean."
  • "The Secure flag ensures the cookie is only transmitted over HTTPS — essential before we go to production."
  • "The auth cookie has a Max-Age of 604800 — that's seven days — so it's a persistent cookie, not a session cookie."

localStorage conversations

  • "We persist the theme preference in localStorage so it survives a page refresh without a round trip to the server."
  • "Don't store the JWT in localStorage — if we ever get an XSS bug, the token can be exfiltrated trivially."
  • "We serialise to JSON before writing to localStorage — it only stores strings, so objects need JSON.stringify."
  • "We're using sessionStorage for the checkout wizard — state clears automatically when the tab is closed."
  • "localStorage is synchronous and blocks the main thread — for anything over a few KB we should move to IndexedDB."
  • "We listen for the storage event to sync the theme preference across open tabs in real time."

Security deep-dive: XSS and CSRF

The choice between cookies and localStorage is ultimately a security trade-off between two attack types: XSS (Cross-Site Scripting) and CSRF (Cross-Site Request Forgery).

XSS allows an attacker to run JavaScript in a victim's browser. Any data in localStorage or a non-HttpOnly cookie can be read and sent to the attacker's server. HttpOnly cookies are immune to this — the browser simply refuses to expose them to JavaScript.

CSRF tricks the victim's browser into making an authenticated request to your server (e.g. by loading a malicious page that submits a form). Because browsers attach cookies automatically, a cookie-authenticated endpoint is vulnerable unless SameSite protection is in place. localStorage values are never sent automatically, so CSRF is not a concern for localStorage-based auth — but this only helps if your frontend explicitly includes the token in every request header, which still requires correct API design.

// Sending a localStorage JWT manually in every request
const token = localStorage.getItem('access_token');
const response = await fetch('/api/profile', {
  headers: {
    'Authorization': `Bearer ${token}`,
  },
});

The modern consensus for web applications is: store authentication tokens in HttpOnly, Secure, SameSite=Strict cookies, pair with a strict Content Security Policy, and use anti-CSRF tokens or the Double Submit Cookie pattern if SameSite=Lax is required for cross-site flows.

Decision guide

  • Authentication token / session ID → HttpOnly, Secure, SameSite=Strict cookie
  • User preference (theme, language, layout) → localStorage
  • Temporary checkout or multi-step form state (per tab) → sessionStorage
  • Data the server must read on every request → Cookie
  • Cached API responses or offline app state → localStorage or IndexedDB
  • Cross-tab state synchronisation → localStorage (the storage event fires across open tabs)
  • Cross-subdomain sharing needed → Cookie with Domain=example.com
  • Large structured data (>500 KB) → IndexedDB (asynchronous, more capable)

Code examples

Setting a cookie from the server (Node.js / Express)

res.cookie('session_id', token, {
  httpOnly: true,    // not accessible to JavaScript
  secure: true,      // HTTPS only
  sameSite: 'strict', // no cross-site sending
  maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days in ms
  path: '/',
});

Reading a cookie in the browser (non-HttpOnly)

// document.cookie returns a semicolon-separated string
const getCookie = (name) => {
  const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
  return match ? decodeURIComponent(match[1]) : null;
};

const lang = getCookie('preferred_lang'); // e.g. 'en-GB'

localStorage with expiry simulation

// localStorage has no native expiry — implement it manually
function setWithExpiry(key, value, ttlMs) {
  const item = { value, expiry: Date.now() + ttlMs };
  localStorage.setItem(key, JSON.stringify(item));
}

function getWithExpiry(key) {
  const raw = localStorage.getItem(key);
  if (!raw) return null;
  const item = JSON.parse(raw);
  if (Date.now() > item.expiry) {
    localStorage.removeItem(key);
    return null;
  }
  return item.value;
}

setWithExpiry('cache_key', { data: [] }, 60_000); // 1 minute TTL

Frequently asked questions

What is the main difference between cookies and localStorage?

Cookies are sent automatically to the server with every HTTP request — the server can read and set them via the Set-Cookie header. localStorage is client-side only and is never sent to the server; it can only be accessed by JavaScript running in the browser. Cookies have configurable expiry and security flags (HttpOnly, Secure, SameSite); localStorage has no expiry and is accessible to any script on the page.

What is sessionStorage?

sessionStorage works like localStorage (JavaScript-only, never sent to the server, up to ~5 MB) but its data is scoped to the current browser tab and is cleared when the tab is closed. It is not shared across tabs, even to the same origin. It is useful for temporary wizard state, multi-step forms, or per-tab data that should not persist.

Can cookies be stolen by an XSS attack?

Yes — unless you set the HttpOnly flag. An HttpOnly cookie is not accessible to JavaScript; it cannot be read via document.cookie, so even if an attacker injects malicious JavaScript, they cannot exfiltrate the cookie value. localStorage has no equivalent protection and is fully readable by any JavaScript on the page, making it unsuitable for sensitive tokens on sites that have not fully mitigated XSS.