The web has a strange foundation: HTTP is stateless, meaning each request arrives with no memory of the ones before it. The server that just logged you in has, by default, forgotten you by your next click. And yet you stay logged in across a whole session. That illusion of memory is built from a small set of mechanisms — cookies, server sessions and tokens — and understanding them is essential to building or securing anything with a login.
Everything starts with the cookie, the humble tool that lets the browser carry a little identifying data from one request to the next.
Cookies: the browser's memory
A cookie is a small piece of data the server asks the browser to store and send back on every subsequent request to that site. That is the whole mechanism that defeats statelessness: the server sets a cookie, the browser returns it automatically, and suddenly the server can recognise a returning visitor. Cookies carry flags that matter enormously for security — HttpOnly hides them from JavaScript (blunting many theft attacks), Secure restricts them to HTTPS, and SameSite limits cross-site sending.
A cookie by itself is just a labelled note the browser carries. What that note contains — a session id or a token — is where the two main authentication styles diverge.
Server sessions: the id in the cookie
In the session approach, the server creates a record of your logged-in state in its own storage and hands the browser a cookie containing only a random session id. On each request the browser sends the id, the server looks up the matching record, and knows who you are. The sensitive data stays on the server; the cookie holds nothing but a meaningless key.
This is simple and secure to reason about, and logging someone out is easy — delete the server-side record. The cost is that the server must store and look up session state, which takes more thought to scale across many machines.
Tokens: the state travels with you
The token approach (often JWTs) flips it: the server signs a token that itself contains the identity and permissions, and the client sends it on each request. The server verifies the signature and trusts the contents without a lookup, which scales beautifully across many servers because no shared session store is needed. The trade-off is that tokens are hard to revoke before they expire, and storing them safely in the browser is genuinely tricky.
There is no universally right answer. Sessions are simple and easy to revoke; tokens are stateless and scale-friendly but demand careful handling. What matters is understanding that all of them are just ways to carry a little trustworthy memory across a protocol that, by design, remembers nothing.
Where a session actually lives, and why that choice matters
'The server stores session state' sounds like one decision but is actually several, and which specific storage a team picks changes the operational character of the whole authentication system. An in-memory session store — a plain object or map living inside the application process — is the simplest possible implementation and the worst one for anything beyond a single server, because a session created on one server instance is invisible to every other instance, which breaks the moment a load balancer distributes requests from the same logged-in user across more than one machine. A shared store — Redis is the overwhelming default choice in practice — solves this by giving every application instance a common place to read and write session data, at the cost of adding a new piece of infrastructure the whole authentication system now depends on being available.
The choice is not purely technical either: a session store that is slow under load becomes a bottleneck on literally every authenticated request across the entire site, because every one of them needs a lookup against it, which makes session-store performance and availability a first-class operational concern rather than an incidental implementation detail nobody needs to think about after the initial build.
Sticky sessions: the workaround that trades one problem for another
Before shared session stores were the obvious default, a common workaround was 'sticky sessions' — configuring the load balancer to always route a given user's requests back to the same server instance that originally created their session, using a cookie or IP hash to make the routing decision. This avoids needing a shared store at all, but it reintroduces exactly the fragility a load balancer is normally meant to eliminate: if that one specific server goes down, every user whose session was stuck to it is logged out simultaneously, and the load balancer can no longer distribute load evenly across instances if some of them are disproportionately loaded up with stuck sessions. Most modern deployments prefer a shared, external session store specifically to avoid this coupling between a user's session and any one particular server's uptime.
Session fixation: an attack that exploits how sessions get created
Session fixation is a specific, well-known attack that targets a subtle mistake in session lifecycle management: if an application reuses the same session identifier across the anonymous-browsing state and the logged-in state, rather than issuing a brand new session id at the moment of successful login, an attacker who can plant a known session id in a victim's browser before they log in can then use that same known id themselves after the victim authenticates, effectively hijacking the now-authenticated session without ever needing to steal a cookie directly. The fix is simple to state and easy to forget in practice: always issue a fresh session identifier at the moment of login, discarding whatever pre-login session id existed, so that any session id an attacker might have planted beforehand becomes worthless the instant the real user actually authenticates.
Session hijacking versus session fixation: a distinction worth keeping straight
Session hijacking is the more general and more commonly discussed threat — an attacker obtains a victim's already-valid session identifier, typically by intercepting unencrypted traffic, exploiting a cross-site scripting vulnerability to read a cookie that should have been protected, or through network-level snooping on an unsecured connection — and reuses it directly to impersonate the victim without needing any credentials at all. The defenses differ meaningfully depending on which specific vector is the actual concern: HTTPS everywhere closes off network interception, HttpOnly cookies close off the XSS-driven cookie-theft vector by keeping the session cookie inaccessible to JavaScript entirely, and short session lifetimes combined with re-authentication for sensitive actions limit how much damage a hijacked session can do even if one of the other defenses fails.
What "logging out everywhere" actually requires
A feature users routinely expect — 'log out of all devices' — is trivial with server-side sessions and genuinely awkward with pure stateless tokens, which is one of the most concrete practical differences between the two models discussed in the original article. With sessions, logging out everywhere means deleting every session record associated with that user from the shared store, which takes effect immediately because every subsequent request from any of that user's devices fails the very next session lookup. A pure stateless token has no equivalent central record to delete — the token remains cryptographically valid until it naturally expires, which is precisely why systems that need reliable global logout either keep a token blocklist (reintroducing exactly the server-side lookup that stateless tokens were meant to avoid) or deliberately keep token lifetimes short enough that the gap between 'user requested logout' and 'the last valid token actually expires' stays acceptably small.
What actually goes into a session record besides identity
A session is often described as though it holds only 'which user this is,' but real production sessions typically carry more: the user's roles or permissions at the time of login, a CSRF token tied to that specific session, timestamps for idle and absolute expiry, and sometimes contextual data like the last-known IP address or device fingerprint used to detect a session that has suspiciously moved somewhere it should not have. Deciding what belongs in the session versus what should be looked up fresh from the database on each request is itself a real design trade-off: caching permissions in the session makes authorization checks cheap but means a permission change does not take effect until the session is refreshed or expires, while looking permissions up fresh on every request guarantees correctness at the cost of an extra database call on every single authenticated request.
Idle timeout versus absolute timeout: two different clocks
Session expiry is rarely governed by a single timer; mature systems track two independent clocks. Idle timeout logs a user out after a period of inactivity, resetting every time a new request arrives, and exists to limit exposure from a session left open on a shared or unattended device. Absolute timeout expires a session after a fixed duration regardless of activity, forcing re-authentication periodically even for a continuously active user, and exists to limit how long any single compromised session credential — however it was obtained — remains usable no matter how consistently it is being used. A system that only implements one of the two leaves a real gap: idle timeout alone lets an actively-used but stolen session persist indefinitely, while absolute timeout alone still leaves a real, if bounded, window of exposure for a session abandoned on a public terminal well before that fixed duration elapses.
Why the session cookie itself needs the same flags as any other
It is easy to focus security attention on how the server stores and validates session data while treating the cookie carrying the session id as an afterthought, but the id itself is the entire credential from the browser's side, and every one of the standard cookie security flags applies to it with full force: HttpOnly so client-side script cannot read it even through an unrelated XSS bug elsewhere on the page, Secure so it is never transmitted over an unencrypted connection where it could be intercepted, and SameSite so it is not automatically attached to requests originating from a different site, which closes off a meaningful class of cross-site request forgery. A perfectly designed server-side session store built on top of a cookie missing any of these flags still leaves the entire session vulnerable through the one piece the server does not fully control: the browser's own default handling of an under-protected cookie.
Rotating a session id after a privilege change
Beyond the login-time rotation that defeats session fixation, good practice rotates the session identifier again at any moment a user's privilege level meaningfully changes — after entering a second authentication factor, after an administrator grants elevated access — so that a session id observed or leaked before that privilege escalation cannot be reused afterward to inherit the newly elevated access it was never actually associated with in the first place.