Cookies, Sessions and Web State

You log into a website, click to the next page, and it still knows you're logged in. That feels utterly ordinary — and it is quietly impossible. Because HTTP is stateless, the server forgets you the instant it finishes each response. Your second click arrives looking exactly like a stranger's. So how does any site remember your login, your shopping cart, your language preference?

The answer is a small, clever hack layered on top of stateless HTTP: the server hands your browser a little token, and the browser dutifully sends it back on every future request. That token is a cookie, and the whole apparatus of logins, carts and "remember me" is built from it. This page is about how state is bolted onto a stateless protocol — cookies, server-side sessions, stateless tokens, and the tradeoffs and security traps that come with each.

The cookie mechanism: two headers

A cookie is just a small named string, and the entire mechanism is two HTTP headers. On a response, the server says "store this":

HTTP/1.1 200 OK Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax; Max-Age=3600

The browser saves it, and from then on automatically attaches it to every request back to that site:

GET /account HTTP/1.1 Host: example.com Cookie: session=abc123

That's the whole loop. The server tucks an identifier into the client, the client carries it back, and statelessness is defeated — the state travels in the request, exactly as HTTP's design demands. Everything else is attributes controlling when and how safely the browser replays that cookie.

AttributeControls
Domain / Pathwhich URLs the cookie is sent to
Expires / Max-Agehow long it lives (absent = session cookie, dies when the browser closes)
Secureonly sent over HTTPS, never plain HTTP
HttpOnlyhidden from JavaScript (document.cookie can't read it)
SameSitewhether it rides along on cross-site requests (Strict/Lax/None)

It comes down to one attribute. A cookie with no Expires/Max-Age is a session cookie: the browser keeps it only in memory and throws it away when you close the browser — perfect for a login that should end when you leave. Add an Expires date or a Max-Age and it becomes a persistent cookie, written to disk and surviving restarts until that time — that's the "keep me logged in for 30 days" checkbox. Confusingly, a session cookie (browser-lifetime) and a session identifier (the topic of the next card) are different things that happen to share a word.

What goes in the cookie? Two philosophies

You now have a channel for carrying a bit of state on every request. The big design question is what to put in it. There are two dominant answers, and the choice shapes your whole backend.

1. Server-side sessions (an opaque id)

The classic approach: the cookie holds only a meaningless random session idsession=abc123 — and the real data lives on the server, in a session store keyed by that id.

cookie: session=abc123 server store: { "abc123": { userId: 42, cart: [7, 19], role: "admin" } }

The cookie is just a claim ticket; the server looks up the real state each request. This is easy to reason about and easy to revoke — delete the store entry and the session is dead instantly. The cost: the server must keep that store, and every request pays a lookup.

2. Stateless tokens (the data itself, signed)

The modern alternative: put the actual data in the cookie (or an Authorization header) — userId=42; role=admin — and cryptographically sign it so the server can detect tampering. A JWT (JSON Web Token) is the common form: some JSON plus a signature the server can verify with its secret key.

token: {"userId":42,"role":"admin"} . <signature>

Now the server holds no session store at all — it just checks the signature and trusts the contents. That's wonderfully scalable, but the flip side is real: because there's nothing stored to delete, a signed token is hard to revoke before it expires. Log someone out and their token still verifies until it times out (which is why token systems keep expiry short and add refresh/blocklist machinery). The tradeoff in one line: sessions are easy to revoke but need a store; tokens need no store but are hard to revoke.

Why a signature is enough

The idea that you can hand the client its own role=admin and still be safe sounds reckless until you see the signature at work. The server signs data with a secret only it knows; any change to the data breaks the signature. The demo uses a toy signing function (a real one is HMAC-SHA256), but the logic — verify before you trust — is exactly right.

// A TOY signature (a real server uses HMAC-SHA256 with a secret key). // Don't use this for anything real — it just illustrates sign/verify. const SECRET = 9973; function sign(data) { let h = 0; const s = data + SECRET; for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) & 0xffffff; return h.toString(16); } function makeToken(data) { return data + "." + sign(data); } function verify(token) { const dot = token.lastIndexOf("."); const data = token.slice(0, dot); const sig = token.slice(dot + 1); return sign(data) === sig ? data : null; // null = tampered / invalid } // Server issues a token to a normal user: const token = makeToken('{"userId":42,"role":"user"}'); console.log("issued token :", token); console.log("verify :", verify(token)); // returns the data -> trusted // An attacker edits the payload to grant themselves admin, keeping the old signature: const forged = token.replace('"role":"user"', '"role":"admin"'); console.log("\nforged token :", forged); console.log("verify :", verify(forged)); // null -> REJECTED, signature no longer matches // The attacker can't re-sign, because they don't know SECRET: console.log("\nThe payload is readable, but not FORGEABLE without the secret.");

The key insight: a signed token is readable but not forgeable. Anyone can decode the JSON (so never put secrets in it!), but nobody can change it and produce a matching signature without the server's key. That's what makes it safe to let the client carry its own identity.

Scaling sessions across many servers

Statelessness let you run a fleet of identical servers — but the moment you store sessions in a server's memory, you've reintroduced state, and a problem. Request 1 creates a session on server A; request 2 gets load-balanced to server B, which has never heard of it, and the user is mysteriously logged out. Two cures:

Stateless tokens sidestep the whole issue: with nothing stored server-side, any server can verify any request with just the shared secret. That property — trivial horizontal scaling — is a big reason tokens became popular for APIs and microservices.

Security: the cookie is a key to your account

A session cookie is a bearer credential: whoever holds it is you, as far as the server can tell. That makes cookies a prime target, and two attacks dominate.

Two misconceptions worth clearing up. First, a cookie is not spyware — it's just a bit of text your browser stores and sends back to the same site. What earned cookies their creepy reputation is a narrow, abused case: third-party tracking cookies set by ad networks embedded across many sites, letting one tracker recognise you everywhere. The mechanism is neutral; the same cookie that tracks you is also the one that keeps you logged in.

Second, and the one that bites engineers: never store a secret in a cookie a script can read. If your auth cookie lacks HttpOnly, a single cross-site-scripting flaw anywhere on the page lets document.cookie exfiltrate the session token, and the attacker walks in as the user. Mark session cookies HttpOnly and Secure, keep genuine secrets out of the browser entirely (put them behind an opaque session id), and remember that a signed token's payload is readable by anyone — signing prevents tampering, not reading.