Cookies vs localStorage vs sessionStorage: Which to Use

Browsers give you three common ways to keep data on the client: cookies, localStorage, and sessionStorage. They look similar at a glance, but they differ in lifetime, size, who can read them, and whether the data is sent to your server. Picking the wrong one leads to bloated requests, surprising data loss, or genuine security holes.

What each mechanism is

A cookie is a small piece of data the server (or client-side script) sets, which the browser then attaches to every matching HTTP request automatically. Cookies predate the Web Storage API and were the original way to maintain state across the stateless HTTP protocol.

localStorage and sessionStorage are the two halves of the Web Storage API. Both expose a simple key/value store of strings, scoped to an origin (scheme plus host plus port). The difference between them is lifetime: localStorage persists until explicitly cleared, while sessionStorage is wiped when the browsing context (the tab) ends.

How they work

Cookies

A server sends a cookie via the Set-Cookie response header; the browser stores it and echoes it back in the Cookie request header on subsequent requests to the same domain and path. You can also read and write non-HttpOnly cookies from JavaScript through document.cookie, though the API is awkward (it is a single semicolon-delimited string). Cookies carry attributes that control their behavior:

  • Expires / Max-Age set how long the cookie lives. Without either, it is a session cookie removed when the browser session ends.
  • Domain / Path scope which requests include the cookie.
  • Secure sends the cookie only over HTTPS.
  • HttpOnly hides the cookie from JavaScript, which mitigates theft via cross-site scripting.
  • SameSite (Strict, Lax, or None) controls whether the cookie is sent on cross-site requests, the main defense against cross-site request forgery.

Web Storage

Both storage objects share the same synchronous string-only API:

localStorage.setItem('theme', 'dark');
const theme = localStorage.getItem('theme');
localStorage.removeItem('theme');
localStorage.clear();

Values must be strings, so objects need JSON.stringify on the way in and JSON.parse on the way out. A handy property here is the storage event: when one tab writes to localStorage, other tabs on the same origin receive a storage event, which is useful for syncing UI state. That event does not fire for sessionStorage across tabs because each tab has its own isolated sessionStorage.

Comparison at a glance

PropertyCookieslocalStoragesessionStorage
LifetimeSet by Expires/Max-AgeUntil clearedUntil the tab closes
Sent to serverYes, on every requestNoNo
Typical size limitAbout 4 KB per cookieSeveral MB per originSeveral MB per origin
Accessible to JSYes, unless HttpOnlyYesYes
ScopeDomain plus pathOriginOrigin, per tab
API styleString parsingKey/value, syncKey/value, sync

Storage quotas are defined by the browser, not by a fixed standard, so treat the multi-megabyte figure as a practical ballpark rather than a guarantee.

Why the differences matter

The single most important distinction is that cookies travel with every HTTP request and Web Storage does not. If you stuff a large value into a cookie, you pay that cost on every page load, image, and API call to that domain, slowing things down for no reason. Web Storage stays on the client, so it never inflates your requests.

The flip side is that data the server needs to see on each request, such as a session identifier, belongs in a cookie precisely because it is transmitted automatically. Web Storage is invisible to the server unless you manually read it and put it in a request body or header.

When to use each

Use cookies when

The server must read the value on each request: session IDs, authentication tokens for traditional server-rendered apps, CSRF tokens, and locale or A/B-test flags that influence server output. For anything holding credentials, set HttpOnly, Secure, and an appropriate SameSite value. Note that OAuth2 and JWT token storage is its own debate; an HttpOnly cookie is generally safer than Web Storage for a bearer token because script cannot read it.

Use localStorage when

You need client-only data that should survive across sessions and tabs: theme and UI preferences, a draft autosave, cached non-sensitive API responses, or a feature-tour "seen" flag. Because it persists indefinitely, do not put anything sensitive or anything you expect to expire there.

Use sessionStorage when

The data is meaningful only for the current tab and should vanish when it closes: a multi-step form's in-progress state, a scroll position, or a one-time redirect target after login. Two tabs of your app get independent sessionStorage, which is exactly right for workflows you do not want bleeding across tabs.

Common pitfalls

  • Storing secrets in Web Storage. Any script on the page, including a compromised third-party dependency, can read localStorage and sessionStorage. A stolen token there is a classic XSS payoff. Cookies marked HttpOnly avoid this specific exposure.
  • Forgetting Web Storage is strings only. Saving an object without JSON.stringify stores the useless literal [object Object]. Always serialize, and wrap JSON.parse in a try/catch for corrupted values.
  • Oversized cookies. Exceeding the roughly 4 KB limit causes the browser to silently drop the cookie, and large cookies bloat every request.
  • Assuming storage is always available. Private-browsing modes and storage-blocking settings can make Web Storage throw on write or behave as ephemeral. Guard access and degrade gracefully.
  • Confusing the two storages' scope. localStorage is shared across all tabs of an origin; sessionStorage is not. A "logged in everywhere" assumption based on sessionStorage will break.
  • Quota errors. Hitting the storage limit throws a QuotaExceededError rather than failing silently, so wrap writes that may grow unbounded.

When you are inspecting tokens that live in any of these stores, a JWT Decoder lets you read claims without trusting a third-party site, and the JWT Builder helps you craft test tokens. For shaping the objects you serialize into storage, a JSON Formatter is handy. If client-side privacy is a priority across your tooling, see our note on client-side dev tools and privacy.

A simple decision rule

Ask one question first: does the server need this value on every request? If yes, use a cookie with the right security attributes. If no, the data is client-only, and you choose between localStorage (persist across sessions) and sessionStorage (drop it when the tab closes). And whatever you choose, never treat any client-side store as a safe vault for secrets.

Frequently Asked Questions

Not inherently. Both localStorage and non-HttpOnly cookies are readable by any JavaScript on the page, so both are vulnerable to cross-site scripting. The advantage of cookies is the HttpOnly attribute, which hides the value from JavaScript entirely. For sensitive tokens, an HttpOnly, Secure cookie is generally safer than localStorage.

A single cookie is limited to roughly 4 KB, and browsers cap the number of cookies per domain. localStorage and sessionStorage allow several megabytes per origin, though the exact quota is set by the browser rather than a fixed standard, so treat it as a practical estimate.

Lifetime and scope. localStorage persists until you explicitly clear it and is shared across all tabs of the same origin. sessionStorage is isolated to a single tab and is cleared when that tab closes. The API is otherwise identical.

No. Both localStorage and sessionStorage store strings only. Use JSON.stringify before saving an object and JSON.parse when reading it back. Saving an object without serializing stores the literal text [object Object], which is useless.

Yes, by default the browser attaches matching cookies to every HTTP request to that domain and path, including requests for images and API calls. That is why large cookies hurt performance, and why client-only data is better kept in localStorage or sessionStorage, which are never transmitted automatically.