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
| Aspect | Cookies | localStorage |
|---|---|---|
| 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.comsends the cookie to all subdomains. - Path — restricts the cookie to a URL path prefix;
Path=/apistops 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
storageevent 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.
Which should I use to store a JWT authentication token?
This is genuinely debated. Storing a JWT in an HttpOnly cookie protects against XSS but requires CSRF mitigation (the SameSite flag helps considerably). Storing in localStorage is vulnerable to XSS but sidesteps CSRF. The modern consensus leans towards HttpOnly, Secure, SameSite=Strict cookies for authentication tokens, paired with a Content Security Policy to reduce XSS surface.
What is the SameSite cookie attribute?
SameSite controls whether a cookie is sent on cross-site requests. SameSite=Strict means the cookie is never sent on cross-site requests — the strongest CSRF protection, but it can break some flows. SameSite=Lax (the browser default since ~2020) sends the cookie on top-level navigations but not on embedded cross-origin sub-requests. SameSite=None means the cookie is always sent but requires the Secure flag.
What is the storage limit of cookies vs localStorage?
Cookies are limited to approximately 4 KB per cookie (and roughly 50 cookies per domain). localStorage typically allows 5–10 MB per origin. sessionStorage has the same limit as localStorage. Because cookies are sent on every HTTP request, keeping them small matters for performance; localStorage is appropriate for larger client-side data that does not need to reach the server.
Can localStorage be accessed from a different subdomain?
No. localStorage is isolated by origin — that is, by the combination of scheme, hostname, and port. Data stored on app.example.com is not accessible from api.example.com or www.example.com. Cookies, by contrast, can be scoped to a parent domain (e.g. Domain=example.com) so subdomains receive them automatically.