Browser Storage Mechanisms
The browser offers five distinct storage mechanisms, each designed for different constraints. Choosing the wrong one leads to performance issues, security vulnerabilities, or data loss. Senior engineers understand the full landscape.
The Storage Landscape
| Storage | Capacity | Persistence | Sent to Server | Sync/Async | Access |
|---|---|---|---|---|---|
| Cookies | ~4KB per cookie | Configurable (session/expiry) | Yes (every request) | Sync | Same-origin + path |
| localStorage | ~5-10MB | Permanent (until cleared) | No | Sync | Same-origin |
| sessionStorage | ~5-10MB | Tab lifetime | No | Sync | Same-origin + same tab |
| IndexedDB | 100s of MB+ | Permanent | No | Async | Same-origin |
| Cache API | 100s of MB+ | Permanent | No | Async | Same-origin |
Cookies
The oldest and most misunderstood storage mechanism.
What Makes Cookies Unique
Cookies are the only storage sent with every HTTP request. This makes them essential for authentication but dangerous for performance:
Every request to your domain:
GET /api/data HTTP/1.1
Cookie: session=abc123; theme=dark; analytics_id=xyz; promo_seen=true; ...50 cookies à 4KB each = 200KB of overhead on every single request â including images, CSS, fonts.
Cookie Attributes That Matter
document.cookie = "session=abc123; " +
"Secure; " + // Only sent over HTTPS
"HttpOnly; " + // Not accessible via JavaScript (XSS protection)
"SameSite=Strict; " + // Not sent on cross-origin requests (CSRF protection)
"Path=/; " + // Sent for all paths
"Max-Age=86400; " + // Expires in 24 hours
"Domain=.example.com"; // Shared across subdomains| Attribute | Purpose | Security Impact |
|---|---|---|
Secure | HTTPS only | Prevents interception on HTTP |
HttpOnly | No JS access | Blocks XSS from stealing cookies |
SameSite=Strict | No cross-origin | Prevents CSRF attacks |
SameSite=Lax | Cross-origin on navigation only | Balances security + usability |
SameSite=None; Secure | Always sent cross-origin | Required for third-party cookies |
When to Use Cookies
- Authentication tokens â
HttpOnly+Secure+SameSitefor session IDs - Server-readable preferences â Language, theme (when the server needs to know)
- Tracking â Third-party cookies (increasingly blocked by browsers)
When NOT to Use Cookies
- General data storage (too small, sent with every request)
- Client-only preferences (use localStorage instead)
- Large data (4KB limit)
localStorage
Persistent key-value storage accessible only from JavaScript:
localStorage.setItem('theme', 'dark');
localStorage.setItem('user', JSON.stringify({ name: 'Alice', role: 'admin' }));
const theme = localStorage.getItem('theme');
const user = JSON.parse(localStorage.getItem('user') ?? '{}');
localStorage.removeItem('theme');
localStorage.clear();The Synchronous Trap
localStorage operations are synchronous and block the main thread. On low-end devices, reading a large value can cause jank:
// â Blocking â reads 5MB string synchronously
const bigData = localStorage.getItem('cached_products');
const products = JSON.parse(bigData); // Parsing 5MB on main thread
// â
Better â use IndexedDB for large data
// or at least defer the parse
requestIdleCallback(() => {
const products = JSON.parse(localStorage.getItem('cached_products'));
});Storage Event â Cross-Tab Sync
window.addEventListener('storage', (event) => {
if (event.key === 'theme') {
applyTheme(event.newValue); // Another tab changed the theme
}
});The storage event fires in other tabs/windows of the same origin â not in the tab that made the change. This is how you sync state across tabs.
When to Use localStorage
- User preferences (theme, sidebar collapsed, sort order)
- Non-sensitive cached data (last search query, recent items)
- Feature flags / A/B test assignments
- Any client-only data that should survive page refresh
When NOT to Use localStorage
- Sensitive data (accessible to any JS on the page, including XSS attacks)
- Large datasets (synchronous I/O, 5-10MB limit)
- Data that needs indexing or querying (use IndexedDB)
- Session-specific data (use sessionStorage)
sessionStorage
Identical API to localStorage, but scoped to the browser tab and session:
sessionStorage.setItem('wizard_step', '3');
sessionStorage.setItem('unsaved_form', JSON.stringify(formData));Key Difference from localStorage
- Tab-scoped â Each tab has its own sessionStorage, even for the same URL
- Session-scoped â Cleared when the tab closes
- Duplicated on tab duplicate â Opening "duplicate tab" copies sessionStorage
When to Use sessionStorage
- Multi-step form wizard state
- Unsaved form data recovery within a session
- Temporary UI state (scroll position, expanded sections)
- One-time-per-session flags (show welcome modal once)
IndexedDB
A full asynchronous transactional database in the browser:
// Modern wrapper pattern (raw API is callback-based)
function openDB(name, version) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(name, version);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('products')) {
const store = db.createObjectStore('products', { keyPath: 'id' });
store.createIndex('category', 'category', { unique: false });
store.createIndex('price', 'price', { unique: false });
}
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async function addProduct(db, product) {
const tx = db.transaction('products', 'readwrite');
const store = tx.objectStore('products');
store.put(product);
return tx.complete;
}
async function getByCategory(db, category) {
const tx = db.transaction('products', 'readonly');
const store = tx.objectStore('products');
const index = store.index('category');
return new Promise((resolve) => {
const request = index.getAll(category);
request.onsuccess = () => resolve(request.result);
});
}Use a Wrapper Library
The raw IndexedDB API is painful. Use idb (by Jake Archibald) for a Promise-based wrapper:
import { openDB } from 'idb';
const db = await openDB('my-app', 1, {
upgrade(db) {
db.createObjectStore('products', { keyPath: 'id' });
},
});
await db.put('products', { id: '1', name: 'Widget', price: 10 });
const product = await db.get('products', '1');
const all = await db.getAll('products');When to Use IndexedDB
- Offline-first applications (PWAs)
- Large datasets (100s of MB)
- Data that needs indexing and querying
- Binary data (files, images, blobs)
- Client-side full-text search
Cache API
Designed for caching HTTP request/response pairs, primarily used with Service Workers:
// Cache a set of resources
const cache = await caches.open('v1');
await cache.addAll([
'/',
'/styles/main.css',
'/scripts/app.js',
'/images/logo.png',
]);
// Cache-first strategy
async function cacheFirst(request) {
const cached = await caches.match(request);
if (cached) return cached;
const response = await fetch(request);
const cache = await caches.open('v1');
cache.put(request, response.clone());
return response;
}
// Network-first strategy
async function networkFirst(request) {
try {
const response = await fetch(request);
const cache = await caches.open('v1');
cache.put(request, response.clone());
return response;
} catch {
return caches.match(request);
}
}Caching Strategies
| Strategy | Behavior | Use For |
|---|---|---|
| Cache First | Check cache â fallback to network | Static assets, fonts, images |
| Network First | Try network â fallback to cache | API data, dynamic content |
| Stale While Revalidate | Serve cache â update cache from network | Semi-dynamic content, app shell |
| Cache Only | Only cache, never network | Offline resources |
| Network Only | Only network, never cache | Real-time data, auth checks |
Security Considerations
| Storage | XSS Vulnerable | CSRF Vulnerable | Recommendation |
|---|---|---|---|
| Cookies (HttpOnly) | No (JS can't read) | Yes (auto-sent) | Use SameSite + CSRF tokens |
| Cookies (non-HttpOnly) | Yes | Yes | Avoid for sensitive data |
| localStorage | Yes (any JS can read) | No (not sent to server) | Never store tokens/secrets |
| sessionStorage | Yes | No | Same as localStorage |
| IndexedDB | Yes | No | Encrypt sensitive data |
Where to Store Auth Tokens
Option 1: HttpOnly Cookie (recommended for web apps)
â
Not accessible via JavaScript (XSS-safe)
â
Automatically sent with requests
â ïļ Requires CSRF protection (SameSite + token)
Option 2: In-memory variable (good for SPAs)
â
Not accessible via XSS (not persisted)
â Lost on page refresh (use refresh token cookie)
Option 3: localStorage (common but risky)
â Readable by any XSS attack
â Must manually attach to requests
â
Simple to implementDecision Framework
Need server access on every request? â Cookie
Auth token? â HttpOnly Secure Cookie
User preferences (client-only)? â localStorage
Session-only temporary data? â sessionStorage
Large structured data? â IndexedDB
HTTP response caching? â Cache API
Offline-first? â IndexedDB + Cache API + Service WorkerInterview Signal
Senior candidates demonstrate:
- Full landscape awareness â Know all five mechanisms, not just cookies and localStorage
- Security reasoning â HttpOnly, SameSite, XSS implications per storage type
- Performance awareness â Synchronous vs async, size limits, network overhead of cookies
- Architecture judgment â Choosing the right storage for the right use case
- Practical patterns â Cross-tab sync with storage events, caching strategies, IndexedDB wrappers