Guide
6 min read

Cookies vs localStorage vs sessionStorage

Three browser storage mechanisms with three very different jobs. Picking the wrong one for auth tokens is one of the most common web security mistakes. Here is exactly when to reach for each.

JWT Decoder

Inspect the contents of any JWT token instantly

Open tool

Three mechanisms, three jobs

Browsers offer three ways to persist data on the client side, and they are not interchangeable:

Cookies

Sent to the server with every matching HTTP request. Controlled by both server and client. The only storage that the server can set directly.

localStorage

Client-only. Persists indefinitely until cleared. Shared across all tabs on the same origin. Up to ~5-10 MB.

sessionStorage

Client-only. Scoped to a single browser tab. Cleared automatically when that tab closes.

Cookies in depth

Cookies are the oldest mechanism and the most configurable. A server sets a cookie with a Set-Cookie response header; the browser stores it and returns it automatically on subsequent requests to matching origins. Their real power lies in their attributes:

AttributeWhat it does
HttpOnlyJavaScript cannot read or modify this cookie. Critical for auth tokens : eliminates XSS theft.
SecureCookie is only sent over HTTPS connections. Never omit this in production.
SameSiteControls when cookies are sent cross-site. Lax (default) blocks most CSRF. Strict blocks all cross-site sends. None requires Secure.
DomainWhich domains receive the cookie. Omitting it restricts the cookie to the exact origin.
PathURL path prefix that must match for the cookie to be sent. Defaults to /.
Expires / Max-AgeWhen the cookie expires. Omitting both makes it a session cookie (deleted when the browser closes).

The main downside: cookies add overhead to every HTTP request (even for images and scripts), and the 4 KB size limit makes them unsuitable for large payloads.

localStorage

localStorage stores key-value pairs (strings only) scoped to the origin (protocol + hostname + port). Data persists until the user or your code explicitly clears it. There is no expiry mechanism.

localStorage.setItem('theme', 'dark')
localStorage.getItem('theme')   // 'dark'
localStorage.removeItem('theme')
localStorage.clear()

Good uses: dark mode preference, language selection, non-sensitive UI state, caching public API responses. Bad uses: access tokens, session identifiers, anything that would be dangerous if stolen by injected JavaScript.

sessionStorage

sessionStorage has the exact same API as localStorage but with two key differences: it is scoped to the tab (not shared across tabs on the same origin), and it is cleared when the tab closes.

Good uses: multi-step form state, unsaved draft content, scroll positions within a session, one-time onboarding flags that should reset if the user opens a new tab.

Side-by-side comparison

CookieslocalStoragesessionStorage
Persistent across tabsYesYesNo
Cleared on tab closeNoNoYes
Sent to server automaticallyYesNoNo
Readable by JavaScriptYes (unless HttpOnly)YesYes
Size limit~4 KB~5-10 MB~5-10 MB
ScopeDomain + PathSame originSame origin + tab
APIdocument.cookielocalStorage.*sessionStorage.*

Where to store auth tokens

This is the question every developer hits when building authentication. Here is the honest answer:

localStorage (avoid for tokens)

Any JavaScript on the page can read localStorage. A single XSS vulnerability exposes every token to the attacker. Third-party scripts (analytics, chat widgets, ads) run in the same origin and have full access.

HttpOnly cookies (recommended)

JavaScript cannot read HttpOnly cookies, so XSS cannot steal the token. The tradeoff: cookies are vulnerable to CSRF (cross-site request forgery) attacks where a malicious site tricks the browser into making authenticated requests. Setting SameSite=Lax or SameSite=Strict eliminates the vast majority of CSRF vectors.

Best practice for JWT-based auth

Store the refresh token in an HttpOnly + Secure + SameSite=Lax cookie. Keep the short-lived access token (15 min expiry) in JavaScript memory only: a closure or reactive store variable. It is gone on page refresh, but the refresh token silently re-issues it. Nothing sensitive ever touches localStorage.

Frequently asked questions

Can JavaScript read an HttpOnly cookie?

No. The HttpOnly flag instructs the browser to never expose the cookie to document.cookie or any other JavaScript API. The browser still sends it with every matching HTTP request, but client-side code has no way to access or exfiltrate it.

What happens to sessionStorage if I duplicate a tab?

Duplicating a tab copies the sessionStorage at the moment of duplication. After that, the two tabs have independent copies. Changes in one tab do not appear in the other, and each is cleared independently when its tab closes.

Is localStorage shared across subdomains?

No. localStorage follows the same-origin policy: the protocol, hostname, and port must all match. Data in localStorage on api.example.com is not accessible from app.example.com. Cookies, by contrast, can be scoped to .example.com to share across subdomains.

Can cookies be stolen by an attacker?

Cookies without HttpOnly can be stolen via XSS. Cookies without Secure can be intercepted over HTTP. Network-level theft is the threat Secure addresses; JavaScript-level theft is what HttpOnly addresses. A cookie with both flags set is far harder to steal than a token in localStorage.

What is the difference between a "session cookie" and sessionStorage?

A session cookie is a regular browser cookie with no Expires or Max-Age attribute. The browser deletes it when the browser session ends (the window closes). sessionStorage is an entirely different API. It stores data in memory per tab, cleared when that tab closes, and has nothing to do with cookies.

Inspect your JWT tokens

Paste any JWT to see its header, payload, claims, and expiry time.

Open JWT Decoder

Related tools