Browser State: Cookies, Sessions and Storage

October 2, 2026 · 7 min read

HTTP is stateless: each request is handled on its own, and the server keeps no memory of the last one. Yet you log in once and stay logged in across pages, tabs and days, and a half-written draft survives a reload. Browsers give you several places to keep that state, and they differ in size, lifetime, scope, speed — and, most importantly, whether the server sees them and whether an injected script can steal them.

This guide starts with the one the server sees (cookies, and the sessions built on them), then covers the four the server doesn't.

A cookie is a small piece of text a server asks the browser to store, and that the browser then sends back on every later request to the same site. The server sets it with a response header:

Set-Cookie: sid=s_8f2c91; HttpOnly; Secure; SameSite=Lax; Max-Age=1209600

And from then on, the browser adds it to requests automatically:

GET /account HTTP/1.1
Host: shop.example
Cookie: sid=s_8f2c91

Your JavaScript doesn't need to do anything. That automatic sending is what makes cookies convenient — and, as we'll see, what makes them a security topic. Scripts can also write cookies, as long as they aren't HttpOnly:

document.cookie = 'theme=dark; Max-Age=31536000; Path=/; Secure; SameSite=Lax';

Sessions: a key, not the data

The usual pattern for login is a session:

browser
server
sessions
browser cookie jar · shop.example
(empty)

HTTP is stateless: each request stands alone, and the server doesn't remember you between them. Cookies are how the browser carries a little state back every time.

0 / 7
  1. You log in. The server checks your password.
  2. It creates a session record on the server — "session s_8f2c91 belongs to user 42" — in a database or a store like Redis.
  3. It sends only the random session ID to the browser as a cookie.
  4. Every later request carries the ID; the server looks it up to find out who you are.

The cookie holds no personal data — just an unguessable key. Logging out deletes the record on the server, which instantly makes the key useless, wherever it might have been copied.

The attributes that matter

AttributeWhat it doesProtects against
HttpOnlyJavaScript can't read the cookieStealing sessions via injected scripts (XSS)
SecureOnly sent over HTTPSEavesdropping on unencrypted connections
SameSite=LaxNot sent on most cross-site requestsOther sites triggering actions as you (CSRF)
Max-Age / ExpiresWhen the browser deletes itSessions living forever
Domain / PathWhich URLs it's sent toLeaking it to parts of the site that don't need it

For a session cookie, HttpOnly; Secure; SameSite=Lax is the sensible default. Browsers now treat cookies without a SameSite attribute as Lax anyway.

CSRF in one paragraph: because cookies are sent automatically, a malicious page could make your browser submit a form to your bank, and the bank would see your valid cookie. SameSite stops the browser attaching the cookie when the request starts on another site. For extra safety, sensitive actions also check a CSRF token or the Origin header — see CSRF explained.

Sessions vs tokens

The common alternative is a token such as a JWT: instead of a key to server-side data, the client holds a signed blob that contains the user's identity, and the server verifies the signature instead of looking anything up.

Server session + cookieSigned token (e.g. JWT)
Where state livesOn the serverInside the token
Lookup per requestYes (fast, cacheable)No — verify a signature
Log out / revokeDelete the session — instantHard until it expires, unless you keep a deny list
Good fitWebsites, same-site appsService-to-service calls, APIs used by many clients

For a normal website with a login, server sessions in an HttpOnly cookie are simpler and safer. Tokens shine when many independent services need to verify identity without sharing a session store. Note that a token can be stored in a cookie too — the two ideas aren't exclusive.

Other things cookies are used for

  • Preferences — theme, language, a dismissed banner.
  • Shopping carts for visitors who haven't logged in.
  • Third-party tracking — cookies set by a different domain embedded on many sites. Browsers increasingly block these by default, which is why you see fewer of them over time.

Cookies are limited to about 4 KB each, and every byte travels with every request. They're for keys and small server-relevant values, not data. For everything else, the browser has storage the server never sees.

Storage the server doesn't see

document.cookie = 'theme=dark; Max-Age=31536000; Secure'
localStorage.setItem('draft', text)
sessionStorage.setItem('step', '2')
const db = await openDB('app') // IndexedDB
const cache = await caches.open('v1') // Cache API
properties
size~4 KB per cookie
sent to serverwith every request
lifetimeuntil it expires
JS accessunless HttpOnly

Cookies are the only storage the browser sends to your server automatically, on every request. That makes them right for session IDs — and wrong for anything big.

0 / 4

localStorage

localStorage.setItem('draft', JSON.stringify(draft));
const draft = JSON.parse(localStorage.getItem('draft') ?? 'null');
  • Strings only — serialise objects with JSON.
  • Around 5 MB per origin, kept until cleared.
  • Shared across all tabs of the same site; other tabs get a storage event when it changes.
  • Synchronous: every read and write blocks the main thread. Fine for small values, bad for large ones.

Good for preferences, small drafts, "last viewed" state.

sessionStorage

Same API as localStorage, but scoped to one tab and cleared when the tab closes. Each tab gets its own copy, so it's handy for per-tab state like the step a user is on in a wizard.

IndexedDB

// with the small 'idb' wrapper library
const db = await openDB('app', 1, {
  upgrade(db) { db.createObjectStore('notes', { keyPath: 'id' }); },
});
await db.put('notes', { id: 1, text: 'Hello', updated: Date.now() });
const note = await db.get('notes', 1);
  • A real database: stores objects, arrays, Blobs and files, with indexes and transactions.
  • Asynchronous, so large reads don't freeze the page — and available inside workers.
  • Can hold a lot: browsers allow a substantial share of free disk space.

The raw API is verbose and event-based, so most people use a small promise wrapper. It's the right home for offline data, caches of API results, and anything big.

Cache API

const cache = await caches.open('v1');
await cache.put('/api/feed', response);
const cached = await cache.match('/api/feed');

Stores whole HTTP Request → Response pairs. It's designed for service workers to serve pages and assets offline, but pages can use it too.

Side by side

CookieslocalStoragesessionStorageIndexedDBCache API
Typical size~4 KB each~5 MB~5 MBLargeLarge
LifetimeUntil expiryUntil clearedTab closesUntil clearedUntil cleared
Sent to serverEvery requestNoNoNoNo
APIString parsingSync, stringsSync, stringsAsync, objectsAsync, responses
Readable by injected scriptsNot if HttpOnlyYesYesYesYes

Security: where not to put secrets

Anything JavaScript can read, an XSS attack can steal. That rules out putting long-lived authentication tokens in localStorage, sessionStorage or IndexedDB: one injected script and they're gone, usable from anywhere. An HttpOnly, Secure, SameSite cookie can't be read by scripts at all, which is why it's the safer home for session credentials.

For sessions themselves:

  • Never put secrets or personal data directly in a cookie value; store a random ID and keep the data on the server.
  • Generate session IDs with a cryptographically secure random generator, and issue a new one after login so an attacker can't plant a known ID beforehand.
  • Expire sessions — both an idle timeout and an absolute maximum.

For browser storage:

  • Storage is per origin, and visible to anyone using the same browser profile. Don't store other people's private data unencrypted.
  • Browsers can evict non-cookie storage when disk space runs low, especially for sites the user rarely visits. Call navigator.storage.persist() to request durable storage for data you can't re-download.
  • Private browsing clears everything when the window closes, and some browsers clear script-written storage for sites that haven't been visited in a while. Treat browser storage as a cache, not the only copy.
  • Every access can throw — storage disabled, quota exceeded, private mode. Wrap it in try/catch and keep working without it.

Choosing

  • Who you are, checked by the server on each request → an HttpOnly session cookie.
  • Other small values the server needs on each request → cookie.
  • Small preference or draft, shared across tabs → localStorage.
  • Per-tab, temporary state → sessionStorage.
  • Lots of structured data, files, offline data → IndexedDB.
  • Offline copies of pages and HTTP responses → Cache API.

The short version: the server remembers, the cookie only reminds it which memory is yours — and everything else the page keeps for itself.