localStorage vs sessionStorage vs Cookies: The Complete Guide for 2026
Author
Muhammad Awais
Published
May 23, 2026
Reading Time
10 min read
Views
20k

The Browser Storage Decision That Breaks More Apps Than Any Other
You are building a feature and you need to persist some data on the client. Maybe it is a user preference, an authentication token, a shopping cart, or a form draft. You open a new file and pause. Should this go in localStorage? sessionStorage? A cookie? You have seen all three used in different codebases and the reasoning behind each choice was never fully explained. You make a call, ship the feature, and six months later someone on your team says the implementation has a security issue. This guide exists so that does not happen to you again. Browser storage is one of those topics that every JavaScript developer uses every day but rarely fully understands because most tutorials explain what each option does without explaining when to use which one and why the wrong choice creates real vulnerabilities. By the end of this guide you will have a clear decision framework, understand the actual security implications of each option, and know exactly how to use them correctly in a Next.js application.
The Four Browser Storage Options Explained
Modern browsers give you four distinct ways to persist data on the client side. Each one exists for a specific reason and has trade-offs the others do not share.
localStorage : Key-value storage that persists indefinitely until explicitly cleared. Data survives browser restarts, tab closes, and system reboots. Scoped to the origin (protocol + domain + port). Storage limit is typically 5 to 10 MB depending on the browser. Accessible only through JavaScript not sent to the server automatically.
sessionStorage : Key-value storage with the same API as localStorage but with one critical difference: data is cleared automatically when the browser tab is closed. Each tab gets its own isolated sessionStorage opening the same URL in a new tab starts with an empty sessionStorage. Same 5 to 10 MB limit. Also JavaScript-only, never sent to the server.
Cookies : Small pieces of data (up to 4 KB each) that the browser automatically sends to the server with every matching HTTP request. Can be configured with expiration dates, domain scope, path scope, security flags, and SameSite policies. The only storage mechanism that the server can read and write directly. The only option that survives cross-tab use while also being available server-side.
IndexedDB : A full client-side database that handles large amounts of structured data, binary files, and complex queries. No practical size limit for most use cases (browser may prompt user for very large allocations). Async API only. Used for offline-capable apps, file caching, and anything that needs real querying capability. Not a replacement for the other three in most cases it solves a different problem entirely.
The confusion between these options comes from the fact that they all technically "store data" but they are designed for completely different use cases. Using the wrong one is not just a code style issue it is a security decision with real consequences.
localStorage: What It Is Actually For
localStorage is designed for user preferences and non-sensitive application state that should persist across sessions. The canonical use cases are theme preference (dark or light mode), language or locale selection, UI layout preferences like sidebar collapsed state, recently viewed items, and draft content the user intentionally saved.
The key characteristic of localStorage is that it persists until cleared and is accessible only through JavaScript. That second property is both a feature and a vulnerability. Because localStorage requires JavaScript to read, it is accessible to any JavaScript running on your page including injected scripts from third-party libraries, ads, or cross-site scripting attacks. Any data in localStorage can be read by any script on your origin. This is why localStorage is categorically the wrong place to store authentication tokens, session identifiers, or any sensitive user data.
The 5 to 10 MB storage limit is generous for the intended use case of preferences and lightweight app state. If you are approaching that limit, it is a signal that you are using localStorage for something it was not designed for. Storing large datasets, cached API responses, or application data at scale belongs in IndexedDB.
One important Next.js detail: localStorage is only available in the browser. Trying to access it during server-side rendering will throw a ReferenceError because window does not exist on the server. Always guard localStorage access with typeof window !== "undefined" or use it inside a useEffect hook where you know the code only runs client-side.
sessionStorage: The Underused Option Most Developers Forget
sessionStorage has the same API as localStorage same getItem, setItem, removeItem, and clear methods but data lives only for the lifetime of the current browser tab. When the tab closes, everything in sessionStorage is automatically deleted.
This makes sessionStorage ideal for a specific category of use case that developers often mishandle with localStorage or cookies: temporary state that should not leak between browser sessions or tabs. Multi-step form data is the best example. If a user fills out three steps of a checkout form and closes the tab, they would expect to start over not have their payment details pre-filled the next day. Storing that data in localStorage means it persists forever unless explicitly cleared. sessionStorage handles it correctly by design.
Other good sessionStorage use cases include: single-session navigation state like the previous page in a wizard flow, temporary search filters or sort preferences, unsaved draft content that should disappear on tab close, and scroll position restoration within a single session.
The tab isolation property of sessionStorage is also useful and underappreciated. If a user opens your app in two tabs simultaneously, each tab has a completely separate sessionStorage. This prevents session state from leaking between parallel browsing contexts, which matters for applications where two independent user flows could conflict.
Cookies: The Only Option for Authentication
Cookies are fundamentally different from localStorage and sessionStorage in one critical way: the browser sends them to the server automatically with every matching HTTP request. This is what makes them the only correct choice for authentication tokens, session identifiers, and any data your server needs to read on each request without JavaScript involvement.
The security properties of cookies come from their configuration flags. An HttpOnly cookie cannot be accessed by JavaScript at all not by your code, not by third-party scripts, not by XSS payloads. The browser sends it to the server, the server reads it, and JavaScript never sees it. This is what makes HttpOnly cookies categorically more secure than localStorage for authentication tokens. A JWT or session token stored in localStorage can be stolen by an XSS attack. The same token in an HttpOnly cookie cannot be read by any script, period.
The Secure flag tells the browser to only send the cookie over HTTPS connections. Always set this flag in production. The SameSite attribute controls when the browser sends the cookie with cross-site requests. SameSite=Strict means the cookie is never sent on cross-site requests. SameSite=Lax allows cookies on top-level navigation but not on embedded requests — this is the browser default in 2026 and protects against most CSRF attacks. SameSite=None allows cross-site requests but requires Secure to be set.
In a Next.js application, you set and read cookies in Route Handlers and Server Actions using the cookies() function from next/headers. This is the correct pattern for authentication: the server sets an HttpOnly Secure cookie on login, the browser sends it automatically on every request, and the server reads and validates it without JavaScript ever touching the token value. If you are working with JWT tokens and need to inspect their structure and claims, our free JWT decoder and verifier tool lets you decode and validate tokens directly in your browser. The deeper patterns for JWT-based authentication in Next.js are covered in our guide on Next.js Edge Runtime JWT authentication with Jose.
⚡ Decode and Verify Your JWT Tokens Free — No Sign-Up Required
The Security Mistake That Gets Apps Hacked
The single most common and damaging browser storage mistake in 2026 is storing authentication tokens JWTs, session tokens, API keys in localStorage. It is easy to understand why developers do it: localStorage is simple, requires no server configuration, and works perfectly in local development. But it exposes those tokens to any JavaScript running on the page.
Cross-site scripting (XSS) is still the most common web vulnerability and it makes localStorage authentication tokens trivially stealable. An attacker who manages to inject JavaScript into your page through a vulnerable third-party dependency, an unsanitized user input, or a compromised CDN can read your entire localStorage with a single line of code and exfiltrate every token it contains. HttpOnly cookies are immune to this attack by design because JavaScript cannot access them at all.
The second common mistake is storing sensitive user data email addresses, phone numbers, payment information, health data in localStorage under the assumption that it is private. It is private from other origins but completely exposed to any script running on your own origin. Third-party analytics libraries, chat widgets, error tracking tools, and A/B testing scripts all run on your origin and have full localStorage access unless you have implemented a strict Content Security Policy.
The security patterns that protect your users in Next.js go beyond storage choices cookie configuration, Content Security Policy, and server-side validation all work together. Our comprehensive guide on Next.js Server Actions security covers the server-side side of this security picture in detail.
The Decision Framework: Which One to Use
After understanding what each option does, the decision becomes straightforward when you apply the right questions:
Does the server need to read this data on every request without JavaScript? Use a cookie. Authentication tokens, session identifiers, and CSRF tokens all belong here. Set HttpOnly and Secure flags always.
Should the data disappear when the tab closes? Use sessionStorage. Multi-step form state, single-session navigation state, and temporary UI data belong here.
Is this a user preference or non-sensitive app state that should persist across sessions? Use localStorage. Theme, language, layout preferences, and recently viewed items belong here.
Is this large structured data, binary files, or does it need querying capability? Use IndexedDB. Offline-first app data, cached file assets, and large datasets belong here.
Is this an authentication token, API key, or any sensitive credential? Never use localStorage or sessionStorage. Always use an HttpOnly Secure cookie managed by the server.
One nuance worth understanding: cookies have a 4 KB size limit per cookie, which is enough for a JWT or session ID but not for larger data structures. If you need to store more data server-side per user, the cookie should contain only a session identifier and the actual data should live in a server-side store like Redis or a database. Our guide on type-safe API validation with Zod in Next.js shows how to validate the data that flows through these storage mechanisms safely on the server side.
Quick Comparison Reference
Here is the full comparison across the properties that matter most in practice:
Capacity: Cookies 4 KB | localStorage 5-10 MB | sessionStorage 5-10 MB | IndexedDB no practical limit
Persistence: Cookies configurable (session or expiry date) | localStorage forever until cleared | sessionStorage tab lifetime only | IndexedDB forever until cleared
Server access: Cookies yes (sent automatically on requests) | localStorage no | sessionStorage no | IndexedDB no
JavaScript access: Cookies yes unless HttpOnly | localStorage yes | sessionStorage yes | IndexedDB yes (async)
XSS immune: Cookies yes with HttpOnly flag | localStorage no | sessionStorage no | IndexedDB no
Cross-tab sharing: Cookies yes | localStorage yes | sessionStorage no (tab-isolated) | IndexedDB yes
Available in SSR (Next.js): Cookies yes via next/headers | localStorage no | sessionStorage no | IndexedDB no
Best use case: Cookies for auth tokens | localStorage for preferences | sessionStorage for temporary form state | IndexedDB for large offline data
Frequently Asked Questions
What is the difference between localStorage and sessionStorage?
Both use the same JavaScript API and have the same 5 to 10 MB storage limit. The only difference is persistence. localStorage data survives indefinitely until explicitly cleared it persists across tab closes, browser restarts, and reboots. sessionStorage data is automatically deleted when the browser tab is closed. Each tab also has its own isolated sessionStorage, meaning two tabs open on the same URL cannot share sessionStorage data. Use localStorage for user preferences you want to remember long-term, and sessionStorage for temporary state that should disappear when the user closes the tab.
Is it safe to store JWT tokens in localStorage?
No. Storing JWT tokens or any authentication credential in localStorage is a security risk. localStorage is accessible to any JavaScript running on your page, which means a cross-site scripting (XSS) attack can read and steal your tokens with a single line of code. The correct approach is to store authentication tokens in HttpOnly cookies, which cannot be accessed by JavaScript at all not even by an attacker who has injected malicious code into your page. The browser sends HttpOnly cookies automatically to the server with every request, which is exactly what authentication tokens need to do.
When should I use cookies instead of localStorage?
Use cookies whenever the server needs to read the data, when security is critical (auth tokens, session IDs), or when you need the data to be available across tabs and server-side simultaneously. Use localStorage when the data is purely for client-side use, is not sensitive, and should persist across browser sessions. The practical rule: authentication always uses cookies with HttpOnly and Secure flags. User preferences and non-sensitive UI state use localStorage. Temporary single-session state uses sessionStorage.
How do I use cookies in Next.js App Router?
In Next.js App Router, you read and write cookies using the cookies() function imported from next/headers. This function is available in Server Components, Route Handlers, and Server Actions. To read a cookie: const cookieStore = await cookies(); const value = cookieStore.get("cookie-name")?.value. To set a cookie with security flags in a Route Handler or Server Action, use cookieStore.set("name", "value", { httpOnly: true, secure: true, sameSite: 'lax' }). Never access localStorage or sessionStorage in Server Components — those APIs only exist in the browser.
What is IndexedDB and when should I use it?
IndexedDB is a full client-side database built into the browser that handles large amounts of structured data, binary files, and complex queries. Unlike localStorage and sessionStorage, there is no practical size limit for most applications. IndexedDB is the right choice for offline-capable apps that need to cache large datasets, applications that handle binary file storage like images or documents, and anything that requires indexing and querying rather than simple key-value lookups. For most web applications that only need to persist preferences or authentication state, localStorage and cookies are simpler and sufficient. IndexedDB solves a different, more complex problem.
Why can I not access localStorage in Next.js server components?
Server Components run on the server where there is no browser environment, so browser-specific APIs like localStorage, sessionStorage, and window do not exist. Trying to access them in a Server Component or during server-side rendering will throw a ReferenceError. To use localStorage in Next.js, access it inside a Client Component (marked with "use client") and always inside a useEffect hook to guarantee the code only runs after the component has mounted in the browser. For data that both the server and client need, use cookies via the next/headers cookies API, which works correctly in the server environment.
Continue Reading
View All HubLevel Up Your Workflow
Free professional tools mentioned in this article
Stripe & PayPal Fee Calculator
Calculate the exact Stripe and PayPal transaction fees for US and UK markets. A free developer tool to estimate SaaS payouts, merchant costs, and revenues.
Word & Character Counter
Free online word and character counter tool. Instantly calculate words, characters, sentences, and reading time for essays, social media, and SEO posts.
JWT Decoder & Verifier
Decode, parse, and verify JWT (JSON Web Tokens) securely in your browser. Validate claims and debug authentication payloads instantly with zero server logs.
AI Prompt Generator
Use our free AI prompt generator to improve AI prompts. The ultimate ChatGPT prompt optimizer and Midjourney prompt maker. Top free AI prompt builder tool.




