Module 04 · API fundamentals · Lesson 12 of 15
Sessions vs tokens
Two ways to remember a login: server-side sessions behind an opaque cookie, or signed tokens verified without a lookup. What each costs in storage, revocation and scaling, and the hybrid most real systems use.
Why it matters
After a user proves who they are, every later request has to carry that proof. How you carry it decides whether you need a shared session store, whether you can log someone out instantly, how many servers can verify a request, and which browser attacks you are exposed to. "Sessions or tokens" is really "where does the state live", and that is a scaling and security decision at once.
The idea
A session stores login state on the server, keyed by a random opaque id that the client holds, almost always in a cookie. Each request looks the id up to find the user. A token (typically a JWT, covered next) carries the state itself, signed by the server, so any server with the verification key can trust it without a lookup.
| Server-side sessions | Self-contained tokens | |
|---|---|---|
| State | On the server; client holds only an id | In the token; server holds the key |
| Per-request cost | One lookup in a shared store | One signature verification, no I/O |
| Revocation | Instant: delete the session | Hard: token is valid until expiry unless you keep a deny list (which reintroduces state) |
| Horizontal scaling | Needs a shared store or sticky sessions | Any server with the key can verify |
| Cross-service use | Each service must reach the store | Token can be passed downstream and verified anywhere |
| Payload | Anything, any size, private | Small, visible to the client (signed, not encrypted) |
| Browser risks | CSRF if cookie-based; mitigated with SameSite and tokens | XSS if stored in JavaScript-readable storage; mitigated with HttpOnly cookies |
| Typical home | Web apps with a single backend | APIs with many services, mobile clients, third-party access |
Revocation is the real difference
Everything else is engineering, but revocation is a trade you cannot avoid. Sessions can be killed instantly because the server owns the state. A stateless token lives until it expires, so a compromised token or a banned user stays valid for its lifetime. The standard compromise is short-lived access tokens (5 to 15 minutes) plus a long-lived refresh token stored server-side: the access token is verified statelessly on every request, and revocation takes effect the next time the client refreshes. Instant revocation of access tokens needs a deny list checked per request, which is a small session store by another name.
Where to store it in a browser
Cookies with HttpOnly, Secure and SameSite=Lax or Strict are the safest home for either a session id or a token: JavaScript cannot read them, so an XSS bug cannot steal them, and SameSite blocks most CSRF. Storing tokens in localStorage is convenient and exposes them to any script on the page. Mobile and server clients keep tokens in secure platform storage and send them in an Authorization: Bearer header.
The hybrid most systems end up with
A session-like refresh token in an HttpOnly cookie or secure storage, exchanged for short-lived access tokens that services verify statelessly. The edge handles the exchange; internal services see only access tokens. This gives stateless scaling for the hot path and server-side control for revocation.
Trade-offs
Sessions are simple, revocable and private at the cost of a shared store that every server and service must reach. Tokens scale across servers and services and carry identity downstream at the cost of delayed revocation and a visible payload. Short expiry plus refresh tokens buys most of both, and adds the refresh flow and its own token store.
In an interview
Say where the state lives and how you revoke: "Short-lived JWT access tokens verified by each service with the public key, issued from a refresh token stored server-side, so logout and bans take effect within fifteen minutes and we can revoke instantly by deleting the refresh token." For a classic web app with one backend, saying "server-side sessions in Redis with an HttpOnly SameSite cookie" is equally strong and simpler.
Check yourself
A security team wants a banned user cut off within seconds, but the API uses stateless JWTs valid for 24 hours. What is the smallest design change that achieves it?