Suggest an editImprove this articleRefine the answer for “Browser storage: cookie, LocalStorage, SessionStorage and IndexedDB”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Browser storage** covers four APIs for keeping data client-side: cookies, LocalStorage, SessionStorage, and IndexedDB. | Storage | Size | Lifetime | Sent to server? | |---|---|---|---| | Cookie | ~4KB | Expires/Max-Age | Yes, every request | | LocalStorage | ~5-10MB | Until deleted | No | | SessionStorage | ~5-10MB | Tab close | No | | IndexedDB | 100MB+ | Until deleted | No | **Key rule:** cookies for auth (with `HttpOnly`), LocalStorage for settings, SessionStorage for tab-only state, IndexedDB for offline or large data.Shown above the full answer for quick recall.Answer (EN)Image**Browser storage** is the collective name for four browser APIs (cookies, LocalStorage, SessionStorage, IndexedDB) that let your app keep data on the client side, each with different size limits, lifetime rules, and one key split: whether data travels to the server automatically or stays local. ## Theory ### TL;DR - Cookies are like postcards: small, stamped, and sent with every HTTP request whether you want it or not - LocalStorage and SessionStorage are notebooks in a drawer: Local stays until you delete it, Session disappears when the tab closes - IndexedDB is a filing cabinet: async, indexed, and built for large structured data - Decision rule: server needs the data? Use a cookie. Tab-only state? SessionStorage. Persistent key-value? LocalStorage. Offline app with real data? IndexedDB. ### Quick example ```js // Cookies travel with every HTTP request automatically document.cookie = "userId=123; path=/; max-age=3600"; // LocalStorage: persists across tabs and restarts, ~5-10MB localStorage.setItem("theme", "dark"); console.log(localStorage.getItem("theme")); // "dark" // SessionStorage: same tab only, cleared on close sessionStorage.setItem("cart", JSON.stringify({ id: 1 })); // Open the Network tab in DevTools: only the cookie appears in request headers // Cookie: userId=123 ``` Only cookies show up in request headers. The other three stay off the wire entirely. ### Key difference Cookies attach to every HTTP request automatically. That is both their power and their cost: you get server-side auth state without any extra code, but you pay in bandwidth and a ~4KB per-cookie cap. LocalStorage, SessionStorage, and IndexedDB never touch the network. They live in origin-scoped storage on disk, which makes reads fast and keeps data private from your server unless you explicitly send it. ### When to use - **Auth tokens and sessions:** cookies with `HttpOnly` and `Secure` flags. JS cannot read an `HttpOnly` cookie, which blocks XSS from stealing tokens. - **User preferences (theme, language):** LocalStorage. Persists across sessions, syncs across tabs via the `storage` event. - **Form draft in one tab:** SessionStorage. Auto-clears on close, no manual cleanup needed. - **Offline app data (messages, todos, product catalog):** IndexedDB. Supports transactions, indexes, cursors, and binary blobs. - **Caching images or files client-side:** IndexedDB again. It is the only option that handles binary data at scale. ### Comparison table | Storage | Size limit | Scope | Lifetime | Sent to server? | API | |---|---|---|---|---|---| | **Cookie** | ~4KB per cookie | Domain + path | Expires / Max-Age | Yes, every request | `document.cookie` | | **LocalStorage** | ~5-10MB per origin | Origin | Until deleted | No | `localStorage` | | **SessionStorage** | ~5-10MB per origin | Origin + tab | Tab/window close | No | `sessionStorage` | | **IndexedDB** | 100MB+ (disk quota) | Origin | Until deleted | No | Async DB API | ### How the browser handles this internally Browsers store LocalStorage and SessionStorage as SQLite-backed files keyed by origin. The rendering engine (Blink in Chrome, Gecko in Firefox) enforces the ~10MB quota. Cookies live in a per-domain cookie jar that the network stack reads before every fetch or XHR, serializing them into the `Cookie:` header before the request leaves the machine. IndexedDB in Chromium uses LevelDB under the hood and has a dynamic quota tied to available disk space, which can reach 50% of total disk in some browsers. ### Common mistakes **Storing JWTs in LocalStorage:** ```js // Wrong: any injected script on the page can read this localStorage.setItem("authToken", "eyJhbGci..."); // Correct: set via server response header, invisible to JS // Set-Cookie: token=eyJhbGci...; HttpOnly; Secure; SameSite=Strict ``` LocalStorage is accessible to any JavaScript on the page. One XSS vulnerability and the token is gone. **Assuming LocalStorage updates propagate instantly across tabs:** ```js // Tab A writes: localStorage.setItem("count", "1"); // Tab B reads the old value immediately // You need the storage event: window.addEventListener("storage", e => { console.log("Updated in another tab:", e.newValue); }); ``` The `storage` event fires in other tabs, not the one that wrote. SessionStorage does not fire it at all. **Not bumping the IndexedDB version on schema change:** ```js // Wrong: version stays at 1 after adding a new object store indexedDB.open("myDB", 1); // onupgradeneeded never fires again // Correct: indexedDB.open("myDB", 2); // triggers onupgradeneeded for migration ``` The `onupgradeneeded` callback only runs when the version number increases. **Using SessionStorage for multi-tab flows:** Each tab gets its own isolated SessionStorage. If a user opens a second tab mid-checkout, the cart from the first tab is not there. Use LocalStorage or BroadcastChannel API for cross-tab state. **Writing many cookies from the client:** `document.cookie` parsing is synchronous. On pages with 50+ cookies, the string manipulation adds up. Prefer setting cookies server-side via `Set-Cookie` headers and only reading them client-side when needed. ### Real-world usage - **next-themes (React/Next.js):** stores the active theme in LocalStorage, reads it on first render to avoid flash of wrong theme - **Redux Persist:** serializes Redux state to LocalStorage or IndexedDB for offline-capable apps - **Auth0 / Okta:** `HttpOnly` cookies for JWTs, LocalStorage only for non-sensitive metadata - **PouchDB:** wraps IndexedDB to give a CouchDB-style API that syncs to a server when back online - **Shopify Storefront:** SessionStorage for guest cart in a single tab, IndexedDB for offline catalog browsing ### Follow-up questions **Q:** What is the security difference between storing a JWT in LocalStorage vs. a cookie? **A:** A cookie with `HttpOnly` and `Secure` flags is not accessible to JavaScript at all, so XSS cannot read it. LocalStorage is fully JS-accessible. Short expiry plus refresh tokens can reduce the window, but an `HttpOnly` cookie is the safer default for auth tokens. **Q:** How does IndexedDB quota work compared to LocalStorage? **A:** LocalStorage has a hard cap around 5-10MB per origin. IndexedDB quota is dynamic: browsers typically allow up to 50% of available disk space, but the storage is best-effort and can be evicted. Use `navigator.storage.persist()` to request durable storage and `navigator.storage.estimate()` to check current usage. **Q:** When does the `storage` event fire, and for which storages? **A:** It fires in other tabs and windows of the same origin when LocalStorage changes. It does not fire in the tab that made the change. SessionStorage changes never trigger this event because SessionStorage is isolated per tab by design. **Q:** How do you migrate data from LocalStorage to IndexedDB without downtime? **A:** Read LocalStorage keys during app init, write them into an IndexedDB transaction in the background, then delete the LocalStorage entries. Keep a versioned flag like `migratedV2: true` so the migration only runs once. Users mid-session see no interruption. **Q (senior):** How does each storage behave in private/incognito mode, and what does `navigator.storage.estimate()` return there? **A:** All storages work during the session but are discarded on window close. Cookies without `Expires` or `Max-Age` become session cookies and disappear. In some browsers the IndexedDB quota in incognito is capped lower than normal mode. `navigator.storage.estimate()` still returns values, but they reflect an ephemeral in-memory store, not real disk. `navigator.storage.persist()` in incognito always returns `false`. ## Examples ### Theme persistence with LocalStorage in React ```js // Saves theme preference across page reloads with no server round-trip const useTheme = () => { const [theme, setTheme] = useState( () => localStorage.getItem("theme") || "light" // lazy init avoids SSR mismatch ); useEffect(() => { localStorage.setItem("theme", theme); document.body.className = theme; }, [theme]); return { theme, toggleTheme: () => setTheme(t => t === "light" ? "dark" : "light"), }; }; // After reload: localStorage.getItem("theme") → "dark", no network request ``` The lazy initializer in `useState` runs only once. Without it you would read LocalStorage on every render, which causes issues with server-side rendering. ### IndexedDB with transactions (cart example) ```js const request = indexedDB.open("cartDB", 1); request.onupgradeneeded = e => { const db = e.target.result; db.createObjectStore("items", { keyPath: "id", autoIncrement: true }); }; request.onsuccess = e => { const db = e.target.result; const tx = db.transaction("items", "readwrite"); const store = tx.objectStore("items"); // Both writes are inside one transaction: either both commit or both roll back store.add({ item: "apple", qty: 2 }); store.add({ item: "banana", qty: 1 }); tx.oncomplete = () => console.log("Cart saved"); tx.onerror = () => console.error("Write failed, possibly disk full"); }; ``` Without a transaction, two sequential `store.add()` calls can race under load. The transaction guarantees atomicity. ### Auth cookie vs. LocalStorage token ```js // Insecure: token exposed to any JS on the page localStorage.setItem("token", "eyJhbGci..."); // Secure: set by server, invisible to JS // Server sends: Set-Cookie: token=eyJhbGci...; HttpOnly; Secure; SameSite=Strict // Client just calls the API, browser attaches cookie automatically const res = await fetch("/api/me"); const user = await res.json(); console.log(user.name); // token never touched by client-side JS ``` I have seen teams switch from LocalStorage tokens to `HttpOnly` cookies after a pen test flagged XSS exposure. The migration takes one afternoon. The peace of mind lasts much longer.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.