Cookies vs Local Storage vs Session Storage

Complete comparison — storage limits, security, persistence, and when to use each

SecurityJune 30, 202616 min readBy Keyur Patel

Every web developer eventually hits this question: "Where do I store this data on the client?" You've got three options — cookies, localStorage, and sessionStorage — and picking the wrong one leads to security holes, lost user data, or bloated HTTP requests.

I've seen teams store JWT tokens in localStorage (vulnerable to XSS), dump entire user profiles into cookies (sent with every request), and wonder why sessionStorage data disappears between tabs. This guide clears up the confusion once and for all.

What Are Web Storage and Cookies?

Browsers provide three built-in mechanisms for storing data on the client side. They all persist key-value pairs but differ significantly in capacity, lifetime, security model, and how they interact with HTTP requests.

  • Cookies — Small pieces of data that the browser automatically sends to the server with every request. Oldest mechanism (since 1994).
  • localStorage — Part of the Web Storage API. Stores data permanently in the browser until explicitly removed. Never sent to the server automatically.
  • sessionStorage — Same API as localStorage, but data only lives for the current browser tab session. Close the tab and it's gone.

They serve different purposes. Using the wrong one doesn't just cause bugs — it can create real security vulnerabilities. Let's look at each one in detail.

Cookies — The Full Breakdown

• What Are Cookies?

What: Small text strings (key=value pairs) that the server sends to the browser via the Set-Cookie header. The browser stores them and automatically includes them in subsequent requests to the same domain.

Why: HTTP is stateless — the server has no memory between requests. Cookies were invented to give servers a way to "remember" users. They're the foundation of sessions, authentication, and personalization.

Capacity: ~4KB per cookie, ~50 cookies per domain (browser-dependent). Total ~180KB-600KB per domain across all cookies.

Persistence: Configurable. Session cookies (no Expires/Max-Age) are deleted when the browser closes. Persistent cookies live until their expiration date.

Access: Automatically sent with every HTTP request to matching domain/path. JavaScript can read/write via document.cookie unless HttpOnly is set.

Security: Supports HttpOnly (blocks JS access), Secure (HTTPS only), SameSite (CSRF protection). The most secure client storage when configured properly.

// Server sets a cookie via response header
Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Strict; Max-Age=86400; Path=/

// Cookie flags explained:
// HttpOnly  → JavaScript cannot access this cookie (prevents XSS theft)
// Secure    → Only sent over HTTPS connections
// SameSite  → Strict (same-site only), Lax (top-level navigations), None (cross-site)
// Max-Age   → Expires in 86400 seconds (24 hours)
// Path      → Cookie only sent for requests to this path and below

localStorage — The Full Breakdown

• What Is localStorage?

What: A synchronous key-value store in the browser. Part of the Web Storage API (introduced in HTML5). Data is stored as strings only — objects must be serialized with JSON.stringify().

Why: Provides much more storage than cookies (~5-10MB vs 4KB) and doesn't get sent with HTTP requests. Perfect for client-only data like user preferences, cached API responses, and UI state.

Capacity: ~5MB in most browsers (Safari), ~10MB in Chrome/Firefox. Per-origin limit (protocol + domain + port).

Persistence: Permanent. Data survives browser restarts, system reboots, and updates. Only cleared by explicit JavaScript code or user clearing browser data.

Access: Accessible only via JavaScript on the same origin. Never sent to the server automatically. Shared across all tabs/windows on the same origin.

Security: Vulnerable to XSS — any JavaScript on the page can read all localStorage data. No built-in encryption. No access control beyond same-origin policy.

// Basic localStorage usage
localStorage.setItem('theme', 'dark');
localStorage.getItem('theme');        // "dark"
localStorage.removeItem('theme');
localStorage.clear();                 // removes everything

// Storing objects (must serialize)
const user = { name: 'Alice', role: 'admin' };
localStorage.setItem('user', JSON.stringify(user));
const stored = JSON.parse(localStorage.getItem('user'));

sessionStorage — The Full Breakdown

• What Is sessionStorage?

What: Identical API to localStorage, but scoped to a single browser tab. Each tab gets its own isolated sessionStorage. Data is cleared when the tab (or window) is closed.

Why: Perfect for data that should not persist after the user is done or leak between tabs. Multi-step forms, temporary search filters, tab-specific UI state.

Capacity: ~5-10MB per origin per tab (same as localStorage). More than enough for temporary state.

Persistence: Tab session only. Survives page reloads and navigation within the same tab, but is deleted when the tab is closed. Browser restarts also clear it.

Access: Same-origin only. JavaScript access within the same tab. Not shared between tabs — even tabs on the same domain get separate sessionStorage instances.

Security: Same XSS vulnerability as localStorage — any script on the page can access it. Slightly better isolation since data doesn't persist and is tab-scoped.

// sessionStorage has the same API as localStorage
sessionStorage.setItem('wizard_step', '3');
sessionStorage.getItem('wizard_step');         // "3"
sessionStorage.removeItem('wizard_step');
sessionStorage.clear();

// Use case: preserve form data during page navigation
const formData = { name: 'Alice', email: 'alice@example.com' };
sessionStorage.setItem('checkout_form', JSON.stringify(formData));

// Data gone when tab closes — no cleanup needed

Complete Comparison Table

Here's a side-by-side comparison of all three storage mechanisms. This is the table I wish I'd had when I started web development.

FeatureCookieslocalStoragesessionStorage
Storage Size~4KB per cookie~5-10MB~5-10MB
ExpirationConfigurable (Max-Age/Expires) or sessionNever (until manually cleared)When tab is closed
Sent with HTTP RequestsYes — every matching requestNo — neverNo — never
Access ScopeAll tabs on same domain/pathAll tabs on same originCurrent tab only
JavaScript AccessYes (unless HttpOnly)Yes — alwaysYes — always
Server AccessYes — automatic with requestsNo — must send manuallyNo — must send manually
Security FeaturesHttpOnly, Secure, SameSiteSame-origin policy onlySame-origin policy only
XSS VulnerableNo (with HttpOnly flag)Yes — alwaysYes — always
CSRF VulnerableYes (mitigated by SameSite)NoNo
Use CaseAuth tokens, sessions, trackingUser preferences, cached dataTemp form data, wizard state
APIdocument.cookie (clunky)localStorage.getItem/setItem (clean)sessionStorage.getItem/setItem (clean)
Created ByServer (Set-Cookie) or JSClient-side JavaScript onlyClient-side JavaScript only

Code Examples: Set, Get, and Remove

Let's see the actual JavaScript API for each storage method. The Web Storage API (localStorage/sessionStorage) is clean and intuitive. The cookie API... not so much.

Cookies (JavaScript)

// SET a cookie
document.cookie = "username=alice; Max-Age=86400; Path=/; Secure; SameSite=Lax";

// SET a cookie with expiration date
const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toUTCString();
document.cookie = `theme=dark; expires=${expires}; Path=/`;

// GET a specific cookie (no built-in method — you have to parse)
function getCookie(name) {
  const cookies = document.cookie.split('; ');
  const cookie = cookies.find(c => c.startsWith(name + '='));
  return cookie ? decodeURIComponent(cookie.split('=')[1]) : null;
}
getCookie('username'); // "alice"

// GET all cookies (returns one giant string)
console.log(document.cookie); // "username=alice; theme=dark"

// REMOVE a cookie (set Max-Age to 0 or past expiration)
document.cookie = "username=; Max-Age=0; Path=/";

// Note: You CANNOT access HttpOnly cookies from JavaScript
// They exist, but document.cookie won't show them

localStorage

// SET
localStorage.setItem('theme', 'dark');
localStorage.setItem('language', 'en');

// SET object (must serialize to string)
const settings = { fontSize: 16, sidebar: true, notifications: false };
localStorage.setItem('settings', JSON.stringify(settings));

// GET
const theme = localStorage.getItem('theme'); // "dark"
const settings = JSON.parse(localStorage.getItem('settings'));

// CHECK if key exists
if (localStorage.getItem('theme') !== null) {
  // key exists
}

// REMOVE single item
localStorage.removeItem('theme');

// REMOVE all items for this origin
localStorage.clear();

// GET storage size (approximate)
const totalBytes = new Blob(Object.values(localStorage)).size;
console.log(`Using ${(totalBytes / 1024).toFixed(1)} KB of localStorage`);

sessionStorage

// SET (identical API to localStorage)
sessionStorage.setItem('wizard_step', '2');
sessionStorage.setItem('search_filters', JSON.stringify({ category: 'shoes', sort: 'price' }));

// GET
const step = sessionStorage.getItem('wizard_step'); // "2"
const filters = JSON.parse(sessionStorage.getItem('search_filters'));

// REMOVE
sessionStorage.removeItem('wizard_step');

// CLEAR all
sessionStorage.clear();

// Practical example: preserve scroll position in a SPA
window.addEventListener('beforeunload', () => {
  sessionStorage.setItem('scrollY', window.scrollY.toString());
});
window.addEventListener('load', () => {
  const saved = sessionStorage.getItem('scrollY');
  if (saved) window.scrollTo(0, parseInt(saved));
});

When to Use What (Decision Guide)

Here's my decision framework. Ask yourself these questions in order:

🍪 Use Cookies When:

  • The server needs to read the data (authentication, sessions)
  • You need HttpOnly protection from XSS attacks
  • You want automatic inclusion in every request without extra code
  • You need fine-grained expiration control
  • You're implementing "remember me" login
  • You need cross-subdomain sharing (Domain flag)

💾 Use localStorage When:

  • Data is client-only (server doesn't need it)
  • You need more than 4KB of storage
  • Data should persist indefinitely (user preferences, theme, language)
  • You want data shared across all tabs on the same origin
  • You're caching non-sensitive API responses for offline use
  • You're storing UI state (collapsed sidebar, dismissed banners)

📋 Use sessionStorage When:

  • Data should not outlive the current tab session
  • You need tab isolation (each tab has different state)
  • You're storing temporary form data or wizard progress
  • You want automatic cleanup (no stale data to worry about)
  • You're storing one-time state (redirect URL after login, flash messages)
  • You need tab-specific navigation history or scroll positions

Security Considerations

This is the section most developers skip — and then regret. The storage mechanism you choose directly impacts your app's attack surface.

• XSS (Cross-Site Scripting) Attacks

Risk: If an attacker injects JavaScript into your page, they can read everything in localStorage and sessionStorage. They can steal tokens, user data, and preferences.

Impact on cookies: HttpOnly cookies are immune — JavaScript cannot read them even during an XSS attack. Regular cookies (without HttpOnly) are just as vulnerable as localStorage.

Mitigation: Use HttpOnly cookies for sensitive data. Set Content-Security-Policy headers. Sanitize all user input. Never use innerHTML with untrusted data.

• CSRF (Cross-Site Request Forgery) Attacks

Risk: Cookies are sent automatically with requests. A malicious site can trigger requests to your API, and the browser will include the user's cookies — authenticating the attacker's request.

Impact on localStorage/sessionStorage: Not vulnerable to CSRF because data is never sent automatically. An attacker's site cannot trigger requests that include your localStorage data.

Mitigation: Use SameSite=Strict or SameSite=Lax on cookies. Implement CSRF tokens for state-changing operations. Validate the Origin/Referer header on the server.

• Token Theft Scenarios

localStorage token theft: Attacker finds XSS vulnerability → injects script → reads localStorage → sends token to their server → full account takeover. Token is valid until it expires.

Cookie token theft: With HttpOnly + Secure + SameSite=Strict, the attacker cannot read the token even with XSS. They can make same-site requests that include the cookie, but cannot exfiltrate the token itself.

Bottom line: For authentication tokens, HttpOnly cookies provide defense-in-depth. localStorage provides zero protection against XSS-based theft.

Security comparison at a glance:

AttackCookies (HttpOnly)localStoragesessionStorage
XSS token theft✅ Protected❌ Vulnerable❌ Vulnerable
CSRF⚠️ Use SameSite✅ Not applicable✅ Not applicable
Man-in-the-middle✅ Secure flag❌ No protection❌ No protection
Third-party script access✅ Blocked❌ Full access❌ Full access

Where to Store JWT Tokens?

This is the most debated topic in frontend security. Let me break down the options honestly, because the "correct" answer depends on your architecture.

Option 1: HttpOnly Cookie (Recommended for most apps)

How: Server sets the JWT in an HttpOnly, Secure, SameSite=Strict cookie. Browser sends it automatically with every request.

Pros: Immune to XSS theft. No JavaScript handling of tokens. Automatic with every request.

Cons: Vulnerable to CSRF (mitigated by SameSite). Limited to same-site requests. Cookie size limits apply. Harder with multiple APIs on different domains.

Best for: Traditional web apps, same-domain APIs, server-rendered apps.

Option 2: localStorage (Common but risky)

How: Store JWT in localStorage after login. Read it and attach to request headers (Authorization: Bearer ...) manually.

Pros: Works with any API domain. No CSRF concerns. Easy to implement. Full control over when token is sent.

Cons: Fully exposed to XSS attacks. Any injected script steals the token instantly. No automatic expiration handling.

Best for: SPAs calling cross-domain APIs where you have strong XSS protections (strict CSP, no user-generated HTML).

Option 3: In-memory variable (Most secure, most complex)

How: Store access token only in a JavaScript variable (e.g., a closure or React state). Use a refresh token in an HttpOnly cookie to get new access tokens.

Pros: Access token disappears on page refresh (XSS window is tiny). Refresh token is HttpOnly (can't be stolen by XSS).

Cons: Token lost on page refresh (requires silent refresh). More complex implementation. Adds latency on first load.

Best for: High-security applications (banking, healthcare) where the extra complexity is justified.

Implementation: HttpOnly cookie approach

// Server (Express.js) - Set JWT as HttpOnly cookie after login
app.post('/login', async (req, res) => {
  const user = await authenticate(req.body);
  const token = jwt.sign({ userId: user.id }, SECRET, { expiresIn: '24h' });
  
  res.cookie('auth_token', token, {
    httpOnly: true,     // JS cannot access
    secure: true,       // HTTPS only
    sameSite: 'strict', // No cross-site requests
    maxAge: 86400000,   // 24 hours in ms
    path: '/',
  });
  
  res.json({ success: true, user: { id: user.id, name: user.name } });
});

// Server - Read JWT from cookie (middleware)
function authMiddleware(req, res, next) {
  const token = req.cookies.auth_token;
  if (!token) return res.status(401).json({ error: 'Not authenticated' });
  
  try {
    req.user = jwt.verify(token, SECRET);
    next();
  } catch {
    res.clearCookie('auth_token');
    res.status(401).json({ error: 'Token expired' });
  }
}

// Client - No token handling needed! Cookie is sent automatically
const response = await fetch('/api/profile', {
  credentials: 'include', // Include cookies in fetch requests
});

Common Mistakes Developers Make

These are the mistakes I see repeatedly in code reviews and production incidents. Most are easy to fix once you know about them.

❌ Storing sensitive tokens in localStorage

Any XSS vulnerability gives attackers full access to tokens. One compromised third-party script and every user's session is stolen. Use HttpOnly cookies for auth tokens.

❌ Using cookies for large client-only data

Cookies are sent with EVERY request. Storing 4KB of preferences means 4KB extra on every API call, image load, and stylesheet fetch. Use localStorage for client-only data.

❌ Forgetting to JSON.parse() when reading from storage

localStorage only stores strings. Reading a stored object without JSON.parse() gives you the string "[object Object]" or raw JSON text. Always parse stored objects.

❌ Not handling storage quota exceeded errors

When localStorage is full, setItem throws a QuotaExceededError. Always wrap storage writes in try/catch, especially when storing cached data or growing datasets.

❌ Setting cookies without Path

Without specifying Path, the cookie defaults to the current URL path. This means a cookie set on /login won't be sent on /dashboard. Always set Path=/ for site-wide cookies.

❌ Assuming sessionStorage persists across tabs

Each tab gets its own sessionStorage. Logging in on one tab doesn't automatically propagate to other tabs. For shared state, use localStorage with a storage event listener.

❌ Not setting SameSite on cookies

Without SameSite, cookies are vulnerable to CSRF attacks. Modern browsers default to Lax, but older browsers send cookies with all cross-site requests. Always set SameSite explicitly.

❌ Relying on client-side expiration for security

Users can modify localStorage and cookie values. Never trust client-side expiration timestamps for security decisions. Always validate tokens server-side.

Best Practices (10 Rules to Follow)

These rules will keep you out of trouble in 99% of cases.

1

Use HttpOnly cookies for authentication tokens

Session IDs, JWTs, and refresh tokens should always be in HttpOnly cookies. This is your primary defense against XSS-based token theft.

2

Always set Secure and SameSite flags on cookies

Secure ensures HTTPS-only transmission. SameSite=Strict or Lax prevents CSRF. There's no good reason to omit these in production.

3

Use localStorage for non-sensitive, persistent data only

Theme preferences, language settings, dismissed UI prompts, cached non-sensitive data. Never auth tokens or PII.

4

Use sessionStorage for temporary, tab-scoped state

Multi-step form progress, temporary filters, one-time redirect URLs. Data you'd lose anyway if the tab closes.

5

Wrap localStorage/sessionStorage access in try/catch

Storage can throw errors: QuotaExceeded, SecurityError (in private browsing on some browsers), or when disabled. Handle gracefully.

6

Always serialize/deserialize objects with JSON

localStorage.setItem('user', JSON.stringify(user)) to write, JSON.parse(localStorage.getItem('user')) to read. Never store raw objects.

7

Set explicit expiration on persistent cookies

Don't rely on session cookies for things that should persist. Use Max-Age for predictable expiration. Match token lifetime to cookie lifetime.

8

Clean up storage when users log out

Clear auth cookies, remove user-specific localStorage data, and call sessionStorage.clear(). Stale data from previous sessions is a security risk.

9

Use the storage event for cross-tab sync

localStorage fires a 'storage' event in other tabs when data changes. Use this to sync logout state, theme changes, or notifications across tabs.

10

Implement Content-Security-Policy to reduce XSS risk

Even if you use localStorage for tokens, a strict CSP significantly reduces the chance of XSS attacks succeeding in the first place.

Frequently Asked Questions

What is the difference between cookies and localStorage?

Cookies are sent with every HTTP request to the server and have a 4KB limit. localStorage stays in the browser only (never sent to the server), offers 5-10MB storage, and persists until manually cleared. Cookies support HttpOnly and Secure flags for security; localStorage has no such protections.

Is localStorage safe for storing tokens?

No — localStorage is vulnerable to XSS attacks. Any JavaScript on the page (including compromised third-party scripts) can read all localStorage data. For auth tokens, use HttpOnly cookies which JavaScript cannot access even during an XSS attack.

What is sessionStorage used for?

sessionStorage stores temporary data for a single browser tab. It's cleared when the tab closes. Common uses: multi-step form progress, temporary search filters, tab-specific UI state, one-time redirect URLs after login, and scroll position preservation.

How much data can each storage method hold?

Cookies: ~4KB per cookie (50 cookies per domain). localStorage: ~5-10MB per origin. sessionStorage: ~5-10MB per origin per tab. These limits vary by browser — Safari tends to be stricter than Chrome/Firefox.

Are cookies sent with every request?

Yes — all cookies matching the domain, path, and security requirements are automatically included in every HTTP request. This includes API calls, image loads, stylesheet fetches — everything. That's why you should keep cookies small and use localStorage for larger client-only data.

Where should I store JWT tokens?

For most web apps, store JWTs in HttpOnly, Secure, SameSite=Strict cookies. This prevents XSS theft (JavaScript can't access them) and mitigates CSRF (SameSite flag). For SPAs calling cross-domain APIs, localStorage with strong CSP headers is a common tradeoff.

What happens to localStorage when the browser is closed?

Nothing — localStorage persists indefinitely. Closing the browser, restarting your computer, or updating the browser does not clear localStorage. Only explicit JavaScript calls (removeItem/clear) or the user manually clearing browser data removes it.

Can cookies be accessed by JavaScript?

Regular cookies: yes, via document.cookie. HttpOnly cookies: no — they are completely invisible to JavaScript. The browser sends them with requests but document.cookie will never show them. This is exactly why HttpOnly cookies are recommended for sensitive data.

Related Articles & Tools

Conclusion

The rules are simpler than they seem: use HttpOnly cookies for auth tokens, localStorage for persistent client-side preferences, and sessionStorage for temporary tab-scoped state. The biggest mistake is treating all three as interchangeable — they're not.

If you take one thing from this article: never store authentication tokens in localStorage unless you have extremely strong XSS protections in place (and even then, think twice). HttpOnly cookies exist precisely for this purpose — use them.

Get the storage choice right early in your project. Migrating auth from localStorage to cookies later means touching every API call, dealing with logged-out users, and probably breaking things along the way. Better to pick correctly from the start.