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
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.
A cookie is just a small named string, and the entire mechanism is two HTTP headers. On a response, the server says "store this":
The browser saves it, and from then on automatically attaches it to every request back to that site:
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.
| Attribute | Controls |
|---|---|
Domain / Path | which URLs the cookie is sent to |
Expires / Max-Age | how long it lives (absent = session cookie, dies when the browser closes) |
Secure | only sent over HTTPS, never plain HTTP |
HttpOnly | hidden from JavaScript (document.cookie can't read it) |
SameSite | whether 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.
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.
The classic approach: the cookie holds only a meaningless random session id —
session=abc123 — and the real data lives on the server, in a session store keyed
by that id.
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.
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.
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.
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.
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.
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.
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.
Secure (never send it over plain HTTP) and
HttpOnly (so injected JavaScript can't read it via document.cookie).
SameSite attribute (don't send the cookie on cross-site requests) and
per-form anti-CSRF tokens.
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.