DNA🌐 Web BrowserBrowser Storage Mechanisms
ðŸĢHatchlingBrowserStorageArchitecture

Browser Storage Mechanisms

Cookies, localStorage, sessionStorage, IndexedDB, Cache API — each exists for a reason. Senior engineers choose based on constraints, not convenience.

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

StorageCapacityPersistenceSent to ServerSync/AsyncAccess
Cookies~4KB per cookieConfigurable (session/expiry)Yes (every request)SyncSame-origin + path
localStorage~5-10MBPermanent (until cleared)NoSyncSame-origin
sessionStorage~5-10MBTab lifetimeNoSyncSame-origin + same tab
IndexedDB100s of MB+PermanentNoAsyncSame-origin
Cache API100s of MB+PermanentNoAsyncSame-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
AttributePurposeSecurity Impact
SecureHTTPS onlyPrevents interception on HTTP
HttpOnlyNo JS accessBlocks XSS from stealing cookies
SameSite=StrictNo cross-originPrevents CSRF attacks
SameSite=LaxCross-origin on navigation onlyBalances security + usability
SameSite=None; SecureAlways sent cross-originRequired for third-party cookies

When to Use Cookies

  • Authentication tokens — HttpOnly + Secure + SameSite for 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

StrategyBehaviorUse For
Cache FirstCheck cache → fallback to networkStatic assets, fonts, images
Network FirstTry network → fallback to cacheAPI data, dynamic content
Stale While RevalidateServe cache → update cache from networkSemi-dynamic content, app shell
Cache OnlyOnly cache, never networkOffline resources
Network OnlyOnly network, never cacheReal-time data, auth checks

Security Considerations

StorageXSS VulnerableCSRF VulnerableRecommendation
Cookies (HttpOnly)No (JS can't read)Yes (auto-sent)Use SameSite + CSRF tokens
Cookies (non-HttpOnly)YesYesAvoid for sensitive data
localStorageYes (any JS can read)No (not sent to server)Never store tokens/secrets
sessionStorageYesNoSame as localStorage
IndexedDBYesNoEncrypt 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 implement

Decision 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 Worker

Interview Signal

Senior candidates demonstrate:

  1. Full landscape awareness — Know all five mechanisms, not just cookies and localStorage
  2. Security reasoning — HttpOnly, SameSite, XSS implications per storage type
  3. Performance awareness — Synchronous vs async, size limits, network overhead of cookies
  4. Architecture judgment — Choosing the right storage for the right use case
  5. Practical patterns — Cross-tab sync with storage events, caching strategies, IndexedDB wrappers