How HTTP Works

Comparison

Cookies vs localStorage vs sessionStorage: What to Use

Cookies vs localStorage vs sessionStorage: request behavior, size limits, expiry, tab scope, partitioning, XSS and CSRF risks, and auth token choices.

Bottom line: Use cookies for state the server needs on requests. Use localStorage for persistent browser-only preferences and sessionStorage for per-tab state. Auth storage depends on who must read the credential.

By How HTTP WorksReview process
Cookies
vs
localStorage / sessionStorage

TL;DR: Use cookies for anything the server needs on every request, localStorage for preferences that should survive a restart, and sessionStorage for per-tab state. For logins, an HttpOnly session cookie plus CSRF protection is the safe default. Any script on your origin can read localStorage and sessionStorage, so neither one protects a token from XSS.

Side By Side

PropertyCookieslocalStoragesessionStorage
Sent on HTTP requestsAutomatically when cookie scope, credentials mode, and browser policy allowNo; application code must send valuesNo; application code must send values
JavaScript accessAllowed unless HttpOnlyRead/writeRead/write
Size4096-octet name-plus-value check in RFC 6265bis; see belowMDN documents up to 5 MiB per originMDN documents up to 5 MiB per origin
ExpiryMax-Age / Expires, or a browser-defined sessionNo built-in expiryEnds with the page session; survives reloads
Sharing across tabsMatching cookies are available to requests from multiple tabs in the same cookie partitionShared for the same origin and storage partitionSeparate per origin and top-level tab
ScopeHost/domain and path rules; partitioning may add a top-level-site keyOrigin, with possible extra browser partitioningOrigin and tab, with possible extra browser partitioning

The request behavior comes from MDN’s cookie guide and Fetch credentials modes; the lifetime and tab rules from MDN’s localStorage and sessionStorage pages. Privacy settings can block or clear any of them, so build for storage being unavailable.

When To Use Each

Cookie: a server-backed session. The server creates a session ID and sets it in a cookie. This example response (trimmed, like the others here) gives it a 30-minute lifetime:

HTTP/1.1 200 OK
Content-Length: 0
Set-Cookie: __Host-session=example-session-id; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=1800

The __Host- prefix makes supporting browsers insist on Secure, Path=/, and no Domain attribute. HttpOnly hides the cookie from scripts, but the browser still sends it on matching requests. See MDN’s attribute and prefix rules. Expire the session on the server too. The cookie’s Max-Age controls when the browser drops it, but only the server can actually end a session.

localStorage: a browser-only preference. Say, the user’s preferred table density, remembered between visits. sessionStorage: a per-tab draft. Say, an unsent filter expression that shouldn’t leak into another tab:

try {
  localStorage.setItem('table-density', 'compact');
  sessionStorage.setItem('draft-filter', 'status:open');
} catch (error) {
  // Continue with in-memory UI state when persistence is unavailable.
  console.warn('Browser storage unavailable:', error.name);
}

WHATWG defines both as string key/value stores. setItem() throws QuotaExceededError when storage is full, and just touching localStorage can throw SecurityError when a policy blocks it (MDN). Wrap access in try, and make sure the table still renders without it.

Size Limits: Bytes Are Not Characters

As of 2026-10-05, RFC 6265bis is still a draft in the RFC Editor’s final review, not a published RFC; see its Datatracker status. Its §5.6 parsing algorithm rejects a cookie whose name plus value exceeds 4096 bytes. The = and the attributes don’t count toward that. It’s a rule about name and value, not a cap on the whole Set-Cookie line, and browsers vary near the edge. Stay well under it and check in DevTools that the browser kept the cookie.

For Web Storage, MDN documents 5 MiB of localStorage and 5 MiB of sessionStorage per origin. That’s for the whole origin, not per key, and it’s a fixed cap rather than a share of disk like IndexedDB gets. The WHATWG spec doesn’t fix a number; its storage algorithm just says setItem() fails when the value can’t be stored. So catch that failure instead of trying to predict capacity from string lengths.

Auth Tokens: XSS And CSRF Are Separate Problems

If your backend can own the session, an HttpOnly, Secure cookie is a good default, because the frontend never needs to read the credential. OWASP’s HTML5 cheat sheet specifically advises against keeping session identifiers in localStorage, since JavaScript can read them. sessionStorage has the same problem. Its shorter lifetime narrows the window, but a script reads it just as easily.

HttpOnly stops a script from stealing the cookie. It doesn’t stop an injected script from acting as the user, because the browser still attaches the cookie to the requests that script makes. Cookie authentication also needs CSRF protection, since the browser attaches the cookie to requests an attacker’s page triggers too. Use your framework’s CSRF mechanism on state-changing requests and pick a SameSite value on purpose. OWASP treats SameSite as one layer with known gaps, such as sibling hosts on the same site, and points out that XSS can get around CSRF defenses.

If your frontend sends a bearer token straight to an API, your script has to be able to read it, and an HttpOnly cookie can’t fill in an Authorization header. Two common options: keep the access token in memory and fetch a new one after a reload, or put a backend in front that holds the API credential and gives the browser a cookie session. If you really need to persist the token, write down why, and keep its lifetime and permissions small. Memory just limits how long the token sticks around; malicious code running in your app can read it all the same.

Sending a token yourself sidesteps classic CSRF, which relies on the browser attaching cookies automatically, as long as the endpoint accepts only that token. If it also accepts a cookie, CSRF is back on the table, and XSS was never off it. Judge the risk by how the server actually authenticates the request, not by which storage API holds the token.

Expiry, Tabs, And Partitioning

localStorage entries never expire on their own. A JWT’s expiry claim decides whether a server accepts the token; the string itself sits in storage until something deletes it. sessionStorage survives reloads and lasts until the page session ends. A tab opened from another page can start with a copy of the opener’s sessionStorage, and from then on the two are independent (MDN). Cookies with no Expires or Max-Age are session cookies, but a browser’s session restore can bring them back (MDN Set-Cookie). So “close the browser to log out” isn’t a way to end a session; revoke it on the server.

An embedded widget may see different storage depending on which site embeds it. MDN’s state-partitioning guide describes Firefox splitting third-party Web Storage by top-level site. For cookies, CHIPS adds a Partitioned attribute (which requires Secure) that gives each top-level site its own cookie jar. Browsers differ here, so test the widget embedded on two different sites, not just by visiting it directly.

The Common Mistake

Moving a login token to sessionStorage and calling it XSS protection. Both Web Storage APIs hand their values to any script on the origin. The other version of this mistake is moving the token into a cookie without HttpOnly. Scripts can still read it, and now the browser sends it automatically too, which opens you to CSRF. Check the actual cookie flags and how the server authenticates before you call the change an improvement.

Check On The Wire And In DevTools

In Chrome, open Application → Storage → Cookies, Local Storage, and Session Storage for your origin. For each cookie, check HttpOnly, Secure, SameSite, expiry, and partition key. Then switch to Network and look at a request’s Cookie header, because a cookie being stored doesn’t mean a given request sent it. Chrome documents the cookie, localStorage, and sessionStorage panels.

To check what the server sends, point this at your staging route:

curl -sS -D - -o /dev/null https://example.com/session

Look for Set-Cookie and its attributes. curl shows what the server sent, not what a browser did with it: acceptance, SameSite enforcement, partitioning and localStorage all need DevTools. And if an HttpOnly cookie is missing from document.cookie, that’s the flag working. Check the request header and the cookie panel instead.

Browse /search