Cookies vs localStorage vs sessionStorage
Compare the three main browser storage mechanisms by lifecycle, capacity, security, and use case so you pick the right tool for tokens, preferences, and state.
What you'll learn
- ✓Lifecycle differences between the three storages
- ✓Capacity and access patterns
- ✓Security implications of each
- ✓Where auth tokens should actually live
- ✓Practical patterns for real apps
Prerequisites
- •Familiar with HTTP
What and Why
Browsers offer three obvious places to store data on the client: cookies, localStorage, and sessionStorage. They look interchangeable in the dev tools, but they behave very differently. Picking the wrong one is the difference between a login that survives a refresh and one that does not, or between a secure session and a hijackable one.
This article compares them on lifecycle, scope, capacity, network behavior, and security.
Mental Model
- Cookies: small, automatically attached to HTTP requests, server-readable, configurable with
HttpOnly,Secure,SameSite. localStorage: large (~5MB), JS-only, persists until cleared.sessionStorage: same API as localStorage, scoped to a single tab and cleared on close.
Cookies localStorage sessionStorage
Sent w/ HTTP yes no no
JS readable unless HttpOnly yes yes
Capacity ~4 KB / cookie ~5 MB ~5 MB
Lifetime cookie expiry forever tab close
Scope domain + path origin origin + tab
Use case auth, CSRF prefs, cache draft form state The single biggest practical difference: cookies travel with every matching request automatically. The other two do not.
Hands-on Example
Setting a secure auth cookie from the server:
Set-Cookie: sid=abc123; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=2592000
HttpOnlyblocks JS access, neutralizing most XSS token theft.Secureprevents transmission over plain HTTP.SameSite=Laxblocks the cookie on cross-site POSTs, mitigating CSRF.
Reading and writing localStorage:
localStorage.setItem('theme', 'dark');
const theme = localStorage.getItem('theme');
localStorage.removeItem('theme');
sessionStorage works identically, but each tab has its own copy:
sessionStorage.setItem('draft', JSON.stringify(formState));
// gone when the tab closes
A typical division of labor in a modern app:
- Session ID or refresh token: cookie,
HttpOnly+Secure+SameSite=Lax. - User preferences (theme, locale):
localStorage. - In-progress form, wizard step:
sessionStorage. - Cached API responses:
localStorageor IndexedDB depending on size.
If you must use a token in JS (for example, sending it as a header), keep it in memory (a JS variable or a React context). That is wiped on refresh but is not exposed to XSS the way localStorage is.
Common Pitfalls
Storing JWTs in localStorage and calling it secure. Any XSS on your origin can read it. HttpOnly cookies are far safer for session-like tokens.
Setting cookies without SameSite. Older browsers default to None, which means CSRF in many setups. Always set it explicitly.
Assuming sessionStorage is cross-tab. It is not. Two tabs of the same site have separate sessionStorage. Use localStorage plus the storage event for cross-tab sync.
Hitting capacity in cookies. Each cookie is around 4 KB, and the total sent per request adds up. Bloated cookies slow every API call.
Storing PII in localStorage on shared devices. It persists until the user explicitly clears it. Browser sign-out does not touch it.
Mixing storage with iframes. Third-party iframes may have storage blocked (Safari, Firefox strict mode). Do not rely on it for critical flows in embedded contexts.
Practical Tips
- Default to
HttpOnlycookies for anything that authenticates the user. Use CSRF tokens orSameSitefor protection. - Keep
localStoragefor non-sensitive UX state. Treat anything you put there as public. - Use
sessionStoragefor per-tab drafts so opening a second tab does not stomp on the first. - Wrap storage in a small utility module so you can swap implementations (e.g., move to IndexedDB) without touching every call site.
- Always JSON-encode and decode explicitly. Storing objects via
toString()is a silent footgun. - Handle quota errors.
setItemthrows when storage is full; private browsing modes sometimes have a 0-byte quota. - Listen for the
storageevent if you want preferences to sync between open tabs.
Wrap-up
Three storages, three jobs. Cookies are for credentials and anything the server needs every request. localStorage is for persistent client state that is safe to expose to JS. sessionStorage is for ephemeral, per-tab state.
Pick deliberately. The wrong choice usually shows up as either a security incident or a confusing UX bug, and both are much harder to fix later than to design correctly now.
Related articles
- Web Content Security Policy Explained
A practical tour of CSP: what each directive does, how nonces and hashes work, how to roll out a policy safely with report-only mode, and the mistakes that quietly weaken it.
- Web CORS Explained In Depth
Understand Cross-Origin Resource Sharing: simple vs preflight requests, credentials, and the headers that decide whether your browser will let your fetch succeed.
- Web JWT vs Session Authentication
Compare JSON Web Tokens with classic server-side sessions across storage, revocation, scaling, and security so you can choose the right model for your app.
- Web Web Performance: Core Web Vitals Guide
A practical guide to Core Web Vitals: LCP, INP, and CLS. How they are measured, what hurts them, and concrete fixes that move the numbers.