Local Storage vs Session Storage
Local Storage vs Session Storage
Definition: Two browser APIs for storing key-value data client-side, differing in how long the data persists.
How It Works
localStoragepersists until explicitly cleared, even after closing the browsersessionStorageclears the moment the tab closes- Both are part of the Web Storage API, exposed as
window.localStorageandwindow.sessionStorage - Both store only strings — objects must be serialized with
JSON.stringify()before writing and parsed withJSON.parse()after reading - Both are scoped per-origin (scheme + host + port), so
https://app.example.comandhttp://app.example.comsee completely separate stores sessionStorageis additionally scoped per-tab: opening the same URL in a second tab gets a fresh, empty store, even thoughlocalStoragewould be shared- Duplicating a tab (or restoring a crashed session in some browsers) copies
sessionStorageinto the new tab; opening a link in a new tab does not - Reads and writes are synchronous and block the main thread, unlike
IndexedDBwhich is asynchronous
Under the Hood
- Both APIs implement the same
Storageinterface:setItem(key, value),getItem(key),removeItem(key),clear(),key(index), and a.lengthproperty - Data is stored on disk (
localStorage) or in memory (sessionStorage, in most implementations) per-origin, not sent automatically with every HTTP request the way cookies are - Typical storage limit is around 5-10MB per origin (varies by browser), versus roughly 4KB per cookie
- Setting a value fires a
storageevent on other tabs/windows sharing the same origin (not the tab that made the change) — useful for cross-tab sync like “logged out in one tab, log out everywhere” - Writing past the quota throws a
DOMException(QuotaExceededError), so production code should wrap writes in try/catch - In “private” or “incognito” browsing modes, storage is typically still writable but wiped when the session ends
Why It Matters
- A common, but risky, place developers store auth tokens or user preferences
- Both are readable by any JavaScript running on the page, including third-party scripts and injected malicious code — there’s no built-in access restriction like
HttpOnlyon cookies - Neither is sent to the server automatically, so server-rendered auth checks (like checking a session cookie on the first HTML request) can’t rely on them
sessionStorage’s per-tab isolation makes it a natural fit for wizard/multi-step form state that shouldn’t leak between tabslocalStorage’s persistence makes it the default choice for things like theme preference, feature flag caches, or “don’t show this dialog again” flags
Common Pitfalls
- Storing sensitive tokens like JWTs in
localStorage— any XSS-injected script can read it, unlike anHttpOnlycookie - Assuming
sessionStoragesurvives a browser crash/restore reliably across all browsers — behavior is inconsistent - Forgetting that storage is per-origin: a switch from
http://tohttps://, or a subdomain change, silently loses access to previously stored data - Not handling
QuotaExceededErrorwhen storage is full or disabled (e.g., some privacy-focused browser settings block Web Storage entirely) - Treating
localStorageas a database — it has no querying, no indexes, no transactions, and blocks the main thread on every read/write - Forgetting values are always strings:
localStorage.getItem('count') + 1produces string concatenation ("51"instead of6), not addition - Not accounting for the
storageevent firing only in other tabs, then being surprised same-tab logic doesn’t react to its own writes
Comparison
localStorage | sessionStorage | Cookie | |
|---|---|---|---|
| Persistence | Until cleared | Until tab closes | Configurable expiry |
| Scope | Per-origin | Per-origin + per-tab | Per-domain (configurable path) |
| Sent to server | Never | Never | Every matching request |
| Capacity | ~5-10MB | ~5-10MB | ~4KB |
| Accessible to JS | Always | Always | Only if not HttpOnly |
| Sync/Async | Synchronous | Synchronous | N/A (header-based) |
Variants
- IndexedDB — an asynchronous, transactional, indexed object database for larger structured data (offline caches, large datasets); the right tool when Web Storage’s size or synchronous-blocking limits become a problem
- Cache API (via Service Workers) — stores request/response pairs for offline-first apps, distinct from key-value storage
- Cookies — the only client-side storage mechanism that’s automatically transmitted to the server, and the only one that supports
HttpOnly(JS-inaccessible) andSecure/SameSiteflags
Best Practices
- Never store raw JWTs, refresh tokens, or PII in either Web Storage API if XSS is a realistic threat — prefer
HttpOnlycookies for auth state - If you must store tokens client-side (e.g., SPA with no backend session), keep the access token short-lived and store the refresh token server-side or in an
HttpOnlycookie - Namespace keys (
app:theme,app:draft-post-123) to avoid collisions with other scripts or future features sharing the same origin - Wrap
setItemcalls in try/catch to handle quota errors and Safari’s private-mode restrictions gracefully - Use
sessionStoragefor anything that should NOT survive a copy-pasted URL opened in a new tab, like a multi-step checkout form’s in-progress state
FAQ
Does clearing cookies also clear localStorage? No — they’re independent stores; “Clear browsing data” in browser settings usually offers separate checkboxes for each.
Can a service worker read localStorage? No, localStorage isn’t available inside service workers (they run in a different global context); use IndexedDB there instead.
Does incognito mode disable Web Storage? No, storage still works within the incognito session; it’s just wiped when the incognito window closes.
Security Deep Dive
- Web Storage has no equivalent to a cookie’s
HttpOnlyflag — there is no way to mark alocalStoragevalue as inaccessible to JavaScript, full stop - This means the entire threat model reduces to: “can an attacker get any JavaScript to run on this page?” If yes (via XSS (Cross-Site Scripting)), they can read every key in storage, no exceptions
- Cookies, by contrast, support defense in depth:
HttpOnlyblocks JS access even under XSS,Secureblocks transmission over plain HTTP, andSameSite=Strict/Laxblocks the cookie from being sent on cross-site requests, mitigating CSRF - The common counter-argument (“cookies are vulnerable to CSRF, storage isn’t”) is true but addresses a different attack — the right modern pattern for SPAs is often an
HttpOnly,SameSite=Strictcookie for the refresh token plus CSRF tokens orSameSiteprotections, not shifting the token intolocalStorageto “avoid CSRF” while reopening it to XSS - Content Security Policy (CSP) headers reduce XSS risk but don’t eliminate it — a strict CSP is a mitigation, not a reason to treat
localStorageas safe for secrets - Browser extensions with broad permissions can also read page-accessible storage, an often-overlooked side channel distinct from XSS
More FAQ
Is sessionStorage safer than localStorage for tokens? Marginally — it limits the exposure window (cleared when the tab closes) and isn’t shared across tabs, but it’s still fully JS-readable, so it doesn’t solve the XSS exposure problem at all.
Can a server read localStorage directly? No — it’s purely client-side; the only way server-side code sees that data is if client JS explicitly sends it in a request (e.g., as an Authorization header).
Why do so many tutorials store JWTs in localStorage anyway? Simplicity — it avoids dealing with CSRF tokens and cookie configuration, and many demo apps never face a real XSS attack. It’s a reasonable tradeoff for a prototype, a risky default for anything handling real user data.
Does Storage.clear() affect other origins or tabs? No — clear() only wipes the calling origin’s store; it has no effect on other domains, and for localStorage it does affect all tabs of that origin since they share the same underlying store.
Debugging Storage in DevTools
- Chrome/Firefox/Edge DevTools all expose an “Application” (Chrome/Edge) or “Storage” (Firefox) panel listing every key-value pair in both
localStorageandsessionStoragefor the current origin, editable live - The
storageevent only fires cross-tab, so testing sync logic requires two tabs open side by side — a single-tab test will never see the event fire, a common source of “it’s not working” confusion during development JSON.parse(localStorage.getItem('key'))throwing onnullis a frequent bug: a missing key returnsnull, notundefinedor{}, andJSON.parse(null)actually succeeds and returnsnull(a quirk, not an error) — butJSON.parse(undefined)throws, which trips people up when they guard with the wrong falsy check- Storage can be fully disabled by users/enterprise policy; feature-detect with a
try { localStorage.setItem(...) } catch {}guard rather than assuming it’s always available before relying on it for critical functionality
Practical Migration Note
Teams that started with localStorage for auth tokens and want to move to HttpOnly cookies typically do it in stages: switch the backend to also set an HttpOnly session/refresh cookie, update the frontend to stop reading/attaching the token manually (the browser handles cookie attachment automatically), then remove the localStorage write once nothing reads it anymore. The tricky part is usually CORS: cookies require credentials: 'include' on fetch/axios calls and a matching Access-Control-Allow-Credentials: true plus a non-wildcard Access-Control-Allow-Origin on the server, which localStorage-based auth never had to worry about since the token traveled in a manually-set header instead of an automatic cookie.
Common Interview Questions
“Where would you store a JWT and why?” A strong answer distinguishes threat models: HttpOnly cookie if XSS is the primary concern (can’t be read by JS, but needs CSRF mitigation), localStorage/memory if the app has no cookie-based session and XSS risk is otherwise well-controlled (strict CSP, no untrusted third-party scripts) — and explicitly rejecting “just use localStorage, it’s simpler” as the full answer.
“What happens to sessionStorage when a user opens a link in a new tab vs. duplicating the tab?” Opening a link in a new tab starts a fresh, empty sessionStorage for that tab; duplicating an existing tab (browser feature) copies the current sessionStorage into the new tab — a subtle distinction worth knowing when debugging “why did my wizard state appear in a new tab.”
“Why can’t a server read localStorage on the first page load?” Because Web Storage is purely client-side and never transmitted in HTTP requests — a server-rendered page that needs to know client state on first load has to rely on cookies (sent automatically) or wait for a client-side JS call after the page loads.
Performance Considerations
- Reading and writing are synchronous, so storing a large blob (a big JSON cache) on every keystroke of a form autosave can visibly janks the UI thread — debounce writes rather than firing on every change
- Some browsers serialize the entire store to disk on write for
localStorage, meaning many small frequent writes can be slower in aggregate than batching into fewer, larger writes - Reading
localStorageon initial page load (common for theme/auth-state hydration) happens before the first paint in many setups, which is exactly why it should stay small — a multi-megabyte read-and-parse can delay first render measurably
Browser Support and Standards Notes
- Web Storage has been supported in every major browser since IE8 (2009) — it’s one of the safest client-side APIs to rely on without feature detection for modern targets, though defensive try/catch around writes is still good practice for privacy-mode edge cases
- The spec is maintained by WHATWG as part of the HTML Living Standard, not a separate spec — it’s considered a baseline web platform feature rather than an evolving one
- Some browser privacy modes (Safari ITP, Firefox Enhanced Tracking Protection in strict mode) partition or limit storage for third-party iframes specifically, which matters for embedded widgets but not for first-party app storage
Framework Integration Notes
- React/Vue/Svelte apps commonly wrap
localStorageaccess in a small hook or composable (useLocalStorage) that syncs a reactive state variable with the store and handles the JSON serialize/parse and error-guard boilerplate in one place, rather than scattering rawgetItem/setItemcalls through components - State management libraries (Redux via
redux-persist, Zustand via itspersistmiddleware) can automatically snapshot store state intolocalStorageon every change and rehydrate on load, effectively using it as a lightweight persistence layer for client-side State Management - Server-side rendering frameworks (Next.js, Nuxt) must guard storage access behind a client-only check (
typeof window !== 'undefined') sincelocalStoragedoesn’t exist during server-side render and accessing it there throws
Related Terms
Example
Saving a user’s dark-mode preference in localStorage so it persists across visits:
// Write
localStorage.setItem('theme', 'dark');
// Read on next visit
const theme = localStorage.getItem('theme') ?? 'light';
document.documentElement.dataset.theme = theme;
// React to changes made in OTHER tabs
window.addEventListener('storage', (e) => {
if (e.key === 'theme') {
document.documentElement.dataset.theme = e.newValue;
}
});
Contrast with a checkout wizard using sessionStorage so an accidentally duplicated tab doesn’t share half-filled payment state:
sessionStorage.setItem('checkout-step', '2');
sessionStorage.setItem('checkout-draft', JSON.stringify({ address, cart }));Referenced by