Storage APIs — localStorage, sessionStorage, IndexedDB, Cookies, Cache API, OPFS
TL;DR
The browser ships six+ storage layers, each with different semantics. localStorage (synchronous, ~5MB, string-only) is the easy default for tiny persistent state. IndexedDB is the async, large, indexed store for real client data. Cookies are for things the server needs (auth, CSRF). Cache API is for service worker responses. OPFS (Origin Private File System) is the newer high-perf binary store. The senior choices: never put tokens in localStorage, never block the main thread with sync storage, respect quotas, and plan for eviction.
Interview Q&A
Q: The six storage layers — what for?
A:
| Storage | Capacity | Sync/Async | Sent to server? | Use for |
|---|---|---|---|---|
| localStorage | ~5-10MB | sync (blocks) | no | tiny UI prefs, theme, draft text |
| sessionStorage | ~5-10MB | sync | no | per-tab state |
| IndexedDB | hundreds of MB+ | async | no | real client data, offline content |
| Cache API | quota-limited | async (Promise) | no | HTTP response caching (service worker) |
| Cookies | ~4KB per, small total | sync (read/write document.cookie) | yes (every request) | auth (httpOnly), CSRF tokens, server-needed flags |
| OPFS | quota-limited | async | no | high-perf binary, virtual filesystem |
| (older) WebSQL | — | — | — | deprecated — don’t use |
Plus framework abstractions: localForage (wraps IndexedDB), Dexie (IndexedDB query layer), Pinia/Redux persist (state manager → storage adapters).
Q: localStorage — when use, when not.
A:
localStorage.setItem("theme", "dark");
const theme = localStorage.getItem("theme") ?? "light";
localStorage.removeItem("theme");
localStorage.clear();
// JSON pattern
localStorage.setItem("user", JSON.stringify(user));
const user = JSON.parse(localStorage.getItem("user") ?? "null");
Pros: trivial API, persists across sessions, isolated per origin.
Cons:
- Synchronous — reads/writes block the main thread. Don’t read/write in tight loops or hot paths.
- String-only — must serialize/parse JSON manually.
- ~5MB cap — exceed and
setItemthrowsQuotaExceededError. - Not for sensitive data — readable by any JS on the origin. Never store auth tokens here; XSS gives away your session.
Use for: theme/locale prefs, last-visited page, “remember me” flag (not the token), draft form input.
Q: sessionStorage — same API, different scope.
A: Same API as localStorage, but cleared when the tab closes (per-tab isolation). Useful for:
- Step-by-step wizard state.
- Per-tab user preferences (“dark mode for this tab”).
- Auth-like context that shouldn’t survive close.
Note: sessionStorage is per-tab, not per-window or per-origin globally. Opening the same URL in a new tab gets a fresh sessionStorage.
Q: IndexedDB — what’s the model?
A: A transactional, indexed, async key-value store with structured data + queries.
const req = indexedDB.open("myDb", 1);
req.onupgradeneeded = (e) => {
const db = (e.target as IDBOpenDBRequest).result;
const store = db.createObjectStore("posts", { keyPath: "id" });
store.createIndex("byDate", "createdAt");
};
req.onsuccess = (e) => {
const db = e.target.result;
const tx = db.transaction("posts", "readwrite");
tx.objectStore("posts").put({ id: 1, title: "Hello", createdAt: Date.now() });
tx.oncomplete = () => console.log("done");
};
The raw API is clunky (callbacks, not promises). Use a wrapper:
idb(Jake Archibald) — promisified IndexedDB. Lightweight, type-safe.- Dexie — full ORM-ish abstraction with queries.
localForage— drop-in replacement forlocalStorageAPI, IndexedDB underneath.
// idb example
import { openDB } from "idb";
const db = await openDB("myDb", 1, {
upgrade(db) {
db.createObjectStore("posts", { keyPath: "id" });
},
});
await db.put("posts", { id: 1, title: "Hello" });
const post = await db.get("posts", 1);
const all = await db.getAll("posts");
Capacity: hundreds of MB+ typically. Supports binary data, transactions, indexes.
Use cases: offline-capable apps (cached posts, drafts), large data (search index, image blobs, model files), structured queries beyond key-value.
Q: Cookies — what makes them different?
A: Cookies are sent on every HTTP request to their domain. That’s the fundamental difference.
// Reading (JS-accessible cookies only)
document.cookie; // "theme=dark; lang=en"
// Setting
document.cookie = "theme=dark; max-age=2592000; path=/; samesite=strict; secure";
Attributes:
max-age/expires— lifetime.path— URL path scope.domain— host scope.secure— HTTPS only.httpOnly— JS cannot read (server-set only). Critical for auth tokens.samesite—strict/lax/none— CSRF protection.
For auth: server sets a httpOnly; secure; samesite=lax cookie. JS can’t read it (so XSS can’t steal); browser sends it automatically with API requests. Always prefer this over storing tokens in localStorage. See ../17_security/.
Capacity: ~4KB per cookie, ~50 cookies per domain typically. Don’t use for app data — use IndexedDB/localStorage. Use cookies for things the server needs (session, CSRF, A/B test bucket).
Q: Cache API — what is it?
A: Programmatic HTTP-response caching, separate from the browser’s HTTP cache. Lives in the service worker context typically.
// Inside a service worker
self.addEventListener("fetch", (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
return cached ?? fetch(event.request).then((response) => {
return caches.open("v1").then((cache) => {
cache.put(event.request, response.clone());
return response;
});
});
})
);
});
You control: which requests cache, how long, when to invalidate. Different from the HTTP cache (Cache-Control headers) — Cache API is yours to manipulate.
Use cases: offline-first apps, app-shell caching, response staleness control, prefetch via worker. Pairs with service workers (next file).
Q: Origin Private File System (OPFS).
A: A high-performance, sandboxed virtual filesystem for the origin. Async API.
const root = await navigator.storage.getDirectory();
const file = await root.getFileHandle("data.bin", { create: true });
const writable = await file.createWritable();
await writable.write(blob);
await writable.close();
const handle = await root.getFileHandle("data.bin");
const f = await handle.getFile();
const buffer = await f.arrayBuffer();
Faster than IndexedDB for binary data; supports streaming writes; no JSON-serialization overhead. Use for:
- Large file caches (PDFs, model files, video chunks).
- Streaming uploads (write chunks as they arrive).
- WASM apps that need an actual filesystem.
Browser support: Chrome 102+, Safari 15.4+, Firefox 111+. Quotas tied to storage quota (see below).
Q: Storage quota — how much can you use?
A: Browsers grant ~5-50% of the disk to the origin’s total storage (IndexedDB + Cache API + OPFS combined). localStorage and cookies are separately small-capped.
const { usage, quota } = await navigator.storage.estimate();
console.log(`Using ${usage} of ${quota} bytes`);
If you’re approaching quota, request persistent storage — tells the browser to avoid evicting your data under disk pressure:
const persistent = await navigator.storage.persist();
console.log("persistent:", persistent); // true if granted
By default, “best-effort” storage can be evicted (LRU). Persistent storage is opt-in and may require user permission for non-installed sites.
Q: How do you handle storage version migrations?
A: IndexedDB has built-in: bump the version, upgrade callback runs:
const db = await openDB("myDb", 2, {
upgrade(db, oldVersion, newVersion, tx) {
if (oldVersion < 1) db.createObjectStore("posts", { keyPath: "id" });
if (oldVersion < 2) db.createObjectStore("drafts", { keyPath: "id" });
},
});
For localStorage, you migrate manually — read the old format, transform, write the new:
const VERSION = "v2";
if (localStorage.getItem("storageVersion") !== VERSION) {
migrate(localStorage);
localStorage.setItem("storageVersion", VERSION);
}
Plan migrations forward; never reformat without a version check.
Q: Cross-tab sync — same-origin tabs sharing state.
A: Multiple options:
storageevent — fires in other tabs whenlocalStoragechanges:window.addEventListener("storage", (e) => { console.log(e.key, e.oldValue, e.newValue); });BroadcastChannel— explicit cross-tab pub/sub:const ch = new BroadcastChannel("auth"); ch.postMessage({ type: "logout" }); ch.addEventListener("message", (e) => { /* ... */ });- Shared workers — multiple tabs share one worker (see 06_web_workers.md).
localStoragepoll — old hack, don’t use.
Common pattern: user logs out in tab A → BroadcastChannel notifies tab B → tab B reloads its auth state.
Gotchas / edge cases
localStorageis synchronous — reading 100 keys in a loop blocks the main thread. Batch via JSON serialize/parse if needed.localStorageevent fires in other tabs, not the originating one — easy to miss.QuotaExceededError— handle forsetItemwrites. Implement LRU eviction or surface to the user.- Private/incognito mode disables or limits storage —
setItemmay throw immediately. Detect and fall back to in-memory. indexedDBblocks across versions — opening v2 while v1 is open in another tab → blocked. Handle theonblockedevent.- Cookies vs
Set-Cookiesamesite=nonerequiressecure(HTTPS); otherwise the browser drops the cookie silently. - Storage limits per third-party iframe — partitioned storage in modern browsers (Chrome’s storage partitioning) gives each (top-frame, embedded-frame) pair its own storage. May surprise embedded apps.
What a senior is expected to say
- “localStorage for tiny UI prefs (sync, ~5MB, string-only). IndexedDB for real client data (async, hundreds of MB, structured + indexed). Cookies for server-needed things (auth, CSRF) — always
httpOnly; secure; samesite=laxfor tokens.” - “Never store auth tokens in localStorage — XSS reads them. Use httpOnly cookies for tokens.”
- “Use a wrapper (idb, Dexie, localForage) for IndexedDB — raw API is callback-based and clunky.”
- “OPFS for high-perf binary; faster than IndexedDB for blobs/streams.”
- “Storage quota is shared across IndexedDB + Cache API + OPFS. Request persistent storage to avoid eviction under disk pressure.”
- “Cross-tab sync: BroadcastChannel for explicit messaging, storage event for localStorage changes. Shared Worker for one-source state across tabs.”
Cross-references
- Service Workers + Cache API: 05_service_workers_pwa.md
- Security (token storage): ../17_security/
- Web Workers + cross-tab via SharedWorker: 06_web_workers.md
- Cookies + CORS interactions: 09_cors_deep.md
Further reading
- MDN — Web Storage API: https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API
- MDN — IndexedDB: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API
idblibrary: https://github.com/jakearchibald/idb- web.dev — Storage for the web: https://web.dev/articles/storage-for-the-web
- MDN — Origin Private File System: https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system