Course outline

1.3

Stateless vs Stateful Servers, Cookies and Sessions

How a stateless protocol remembers you: sessions, cookies, tokens, and what breaks when you scale.

34 min Reading + animations

Tip: tap or hover any dotted-underlined term to see what it means.

By the end of this lesson you will be able to

  • Explain what “HTTP is stateless” means, and the difference between a stateful and a stateless server.
  • Show why in-memory sessions break when you add a second server, and compare the three fixes: sticky sessions, a central session store, and stateless tokens.
  • Describe exactly how cookies work, including session vs persistent cookies and the attributes Domain, Path, Expires/Max-Age, Secure, HttpOnly and SameSite.
  • Walk through a complete login with a server-side session: creation, lookup, expiry, logout and ID rotation.
  • Read a JWT, explain its signature and expiry, and handle its weak spot (revocation) with short lifetimes and refresh tokens.
  • Explain XSS and CSRF attacks step by step, choose between a cookie and localStorage for auth, and recognise when a system is genuinely stateful (WebSocket).

#The problem: HTTP forgets

HTTP is stateless by design. Every request is a complete, independent message, and the protocol itself keeps no memory of earlier ones. When you log in and then open the next page, nothing in HTTP says “this is the same person who just logged in”.

That design is deliberate. A server that does not have to remember anything between requests is simple, can restart at any moment, and can be copied a hundred times behind a load balancer. But almost every real application does need to remember things: who you are, what is in your cart, which language you chose.

So the state has to live somewhere: in the server’s memory, in a shared store, or in the client, travelling along with every request. Which one you choose is one of the most important decisions in system design, and the subject of this lesson.

#Stateful vs stateless servers

The HTTP protocol is stateless, but the server application on top of it can be designed either way.

Stateless server:
Req 1 (User ID + Token)  ───► [Server] ───► Database lookup ───► Res
Req 2 (User ID + Token)  ───► [Server] ───► Database lookup ───► Res
(nothing is kept in the server's memory)

Stateful server:
Req 1 (Login)            ───► [Server] ───► Save session in RAM/disk
Req 2 (Get profile)      ───► [Server] ───► Read session from RAM ──► Res
(the server remembers the client's history)

Stateless vs stateful: where does the memory live?

Same two requests, two designs. Watch the badges: they show what each side remembers.

Scenario

The server remembers you between requests.

Client
ServerRAM
Database
POST /login (rahim, ••••)
Step 1 of 7
1

Request 1: you log in.

#Stateful server

In a stateful architecture the server keeps the state of each client’s previous requests (session data) in its own memory: RAM or an in-process store.

  • How it works: when the client logs in, the server creates a session and keeps it in memory. On every later request it checks its own memory to know who the client is and what state they are in.
  • Advantage: no heavy authentication or resending of data on each request. Reading RAM is the fastest lookup there is.
  • Disadvantages:
    • Horizontal scaling problem. Suppose you have three servers (A, B, C). The user logs in on Server A, so the session lives in A’s RAM. If the load balancer sends the next request to Server B, B does not recognise the user, unless you add sticky sessions or a centralised session store.
    • Memory consumption. As users grow, server RAM fills up quickly.
    • Fragility. A restart, crash or deploy wipes RAM and logs everyone out.

#Stateless server

In a stateless architecture the server keeps no history or memory of the client between requests. Every request is treated as completely independent.

  • How it works: the client sends its own proof of identity with every request, for example a JWT or an API key. The server only validates it and answers; it saves nothing in memory.
  • Advantages:
    • Excellent scalability. With no state on any server, you can replace 1 server with 100 and any server can process any request.
    • Fault tolerance. If a server crashes, no client data is lost, because the state is with the client (or in a shared database).
  • Disadvantage: every request carries extra data (the token), which slightly increases the network payload, and every request has to be verified.
FeatureStateful serverStateless server
Where the state livesIn server memoryIn the token the client sends (or a shared store)
ScalabilityHard and complicated to scaleVery easy (ideal for microservices)
Server memoryGrows with the number of clientsNo extra load per client
Server crash or deploySessions on it are lostNothing is lost
Load balancingNeeds sticky sessions or a shared storeAny server, any request
Per-request costSmall ID; fast memory lookupLarger token; signature check
Logout / revoke accessEasy: delete the sessionHard: token valid until it expires
Typical usesReal-time multiplayer games, chat rooms, WebSocket connectionsREST APIs, modern web and mobile backends

#Scaling: where should the session live?

The moment you run more than one server behind a load balancer, in-memory sessions break. There are three standard fixes.

Sessions behind a load balancer

Three identical servers. Where should Rahim’s session live so that any of them can serve him?

Scenario

Rahim
Load balancer
Server ARAM: abc → Rahim
Server B
Server C
Session storeRedis
POST /login
Step 1 of 4
1

Rahim logs in. The load balancer happens to send him to Server A, which stores his session in its own RAM.

ApproachHowProsCons
Sticky sessions (session affinity)The load balancer always sends a user to the same server, using a cookie or the client’s IPNo code change; fastUneven load; a server crash or deploy logs its users out; adding servers does not help existing users
Central session storeSessions live in Redis (or a database) shared by all serversApp servers become stateless; easy logout and revocationOne extra network hop (well under a millisecond with Redis); the store must be highly available
Stateless tokens (JWT)The signed session data travels with the clientNo lookup, no shared store; scales anywhereHard to revoke; token size; secret key management

In practice, most web applications use a central store (Redis with an expiry on each session) or tokens, and keep sticky sessions only for connections that are stateful by nature, like WebSockets.

#Cookies

Since HTTP is stateless, after a user logs in and opens the next page, the server forgets who they are. Cookies and sessions solve this problem together.

A cookie is a small piece of text data (at most about 4 KB each) that the browser stores for a website.

  • How it works: in a response, the server sends a header Set-Cookie: key=value, instructing the browser to save it. The browser stores it in its cookie jar. From then on, for every request to that domain, the browser automatically sends the cookie back in a Cookie header.
HTTP/1.1 200 OK
Set-Cookie: session_id=xyz987; Path=/; HttpOnly; Secure; SameSite=Lax
Set-Cookie: lang=bn; Max-Age=2592000; Path=/
GET /dashboard HTTP/1.1
Host: shop.example.com
Cookie: session_id=xyz987; lang=bn
  • Session cookie: has no Expires or Max-Age; deleted when the browser is closed.
  • Persistent cookie: lives until a set expiry date (Expires=<date>) or for a number of seconds (Max-Age=<seconds>), surviving browser restarts. “Remember me” uses one.

Cookies are also called first-party (set by the site in your address bar) or third-party (set by another domain embedded in the page, such as an ad network). Modern browsers increasingly block third-party cookies, which is why cross-site tracking and some embedded logins have changed.

AttributeWhat it doesRecommended for a session cookie
Expires / Max-AgeWhen the cookie dies. Missing = session cookie. Max-Age=0 deletes itLeave out, or keep short
DomainWhich hosts receive it. Missing = only the exact host that set it. Domain=example.com includes all subdomainsLeave out (host-only)
PathOnly sent for URLs under this pathPath=/
SecureOnly sent over HTTPS, never plain HTTPAlways
HttpOnlyJavaScript cannot read it (document.cookie), which stops XSS scripts from stealing itAlways
SameSiteWhether it is sent on requests started by other sites: Strict, Lax or NoneLax (or Strict)

The three SameSite values:

  • Strict: never sent on a cross-site request, not even when you click a link to the site from elsewhere (you arrive looking logged out).
  • Lax: not sent on cross-site sub-requests or POSTs, but sent when you navigate to the site with a top-level GET (clicking a link). This is the default in modern browsers when the attribute is missing.
  • None: always sent, including cross-site; must be combined with Secure. Needed for genuine cross-site uses such as an embedded widget or a payment iframe.

#Sessions

A session is a data structure stored on the server side.

  • How it works: when the user logs in with a correct username and password, the server creates a unique record in its backend (RAM, a database or Redis) holding the user’s sensitive information: user ID, role, and so on.
  • The session ID connection: the server generates a long, random, unguessable string for this session, called the session ID, and sends it to the browser in a cookie. The real data stays on the server; the client holds only a key (the session ID).
Session flow:
[Client] ─── 1. POST /login (credentials) ─────────────► [Server]
[Client] ◄── 2. Response + Set-Cookie: SessionID=123 ─── [Server] (saves 123 in RAM/DB)
[Client] ─── 3. GET /profile + Cookie: SessionID=123 ──► [Server] (finds 123 in DB)

#The complete workflow

  1. The user submits the login form (POST /login).
  2. The server verifies the password and creates a session in its store (for example session_id: xyz987).
  3. The server responds with Set-Cookie: session_id=xyz987; HttpOnly.
  4. The browser saves xyz987 as a cookie.
  5. Later, when the user opens the dashboard (GET /dashboard), the browser sends the header Cookie: session_id=xyz987.
  6. The server looks up xyz987 in its store, finds whose session it is, identifies the user, and returns their data.

Login with a session cookie, step by step

The data stays on the server; the browser only holds the key to it.

Scenario

Browser
Server
Session storeRedis / database
POST /login {user: rahim, password: ••••}
Step 1 of 8
1

1. The user submits the login form over HTTPS.

#Getting sessions right

  • Session IDs must be random and long (at least 128 bits from a cryptographic random generator). Never use a user ID, a counter or a timestamp: those can be guessed.
  • Expire sessions: an idle timeout (e.g. 30 minutes without activity) and an absolute timeout (e.g. 7 days, then log in again). In Redis, a TTL on the key does this for free.
  • Rotate the session ID at login and whenever privileges change, so an ID planted before login (session fixation) becomes useless.
  • Logout must delete the session on the server, not just the cookie in the browser. A copied cookie must stop working.

Self-checkQuestion

Your app runs on three servers behind a round-robin load balancer and stores sessions in each server’s RAM. What do users experience?

#Tokens: JWT and API keys

The stateless alternative to sessions is a token that carries the user’s identity itself.

#Anatomy of a JWT

A JWT (JSON Web Token) is three base64url-encoded parts joined by dots: header.payload.signature.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsInJvbGUiOiJjdXN0b21lciIsImV4cCI6MTc2MTMwMDAwMH0.k3Fq…
└──────────────── header ──────────────┘ └──────────────────────── payload ─────────────────────────┘ └ signature ┘
// header
{ "alg": "HS256", "typ": "JWT" }

// payload ("claims")
{ "sub": "42", "role": "customer", "iat": 1761299100, "exp": 1761300000 }
  • Header: the signing algorithm.
  • Payload: claims about the user: sub (subject, the user ID), role, iat (issued at), exp (expiry, as a Unix timestamp).
  • Signature: computed over header + payload with a key. With HS256 it is an HMAC using one shared secret; with RS256 or ES256 the auth server signs with a private key and any service can verify with the public key.

If anyone changes a single character of the payload, the signature no longer matches, so the server rejects the token. But the payload is only encoded, not encrypted: anyone can decode and read it. Never put passwords or personal secrets in a JWT.

JWT: a signed ID card

Anyone can read a JWT; only the holder of the key can create a valid one.

Scenario

Client
Auth serversigns tokens
API serververifies tokens
POST /login
Step 1 of 6
1

The client logs in with username and password.

#Expiry, refresh tokens and revocation

The great strength of a JWT (no lookup) is also its weakness: the server cannot “delete” a token it has issued. A stolen token works until exp. The standard design therefore uses two tokens:

Access tokenRefresh token
LifetimeShort: 5–15 minutesLong: days to weeks
Sent withEvery API requestOnly to the token endpoint
Checked bySignature + expiry, no databaseDatabase lookup, so it can be revoked
If stolenUsable for minutesRevoke it; rotation detects reuse

To log a user out “everywhere”, revoke their refresh tokens (no new access tokens will be issued) and, for urgent cases, keep a small deny list of token IDs (jti) that the APIs check until those tokens expire.

API keys are a simpler kind of token for machine clients (a server calling a payment or maps API). They are long random strings, sent in a header, looked up on every request, and revoked by deleting them on the server.

Server-side session (cookie with ID)JWT (self-contained token)

  • State on the server (Redis/DB); the client holds a random ID
  • One lookup per request
  • Instant logout and revocation: delete the session
  • Small cookie
  • Natural fit for browser-based websites
  • State inside the signed token, on the client
  • No lookup: verify the signature
  • Revocation is hard: short expiry + refresh tokens
  • Bigger: hundreds of bytes on every request
  • Natural fit for APIs, mobile apps and service-to-service calls

#Security: XSS and CSRF

Cookies are sent automatically and tokens must be stored somewhere. Both facts are exploited by the two classic web attacks.

#XSS (cross-site scripting)

The attacker gets their JavaScript to run inside your site’s pages, for example through a comment that the site displays without escaping. That script runs with the site’s full privileges in the victim’s browser.

XSS: when the attacker’s script runs on your site

Cross-site scripting injects JavaScript into a trusted page. What can that script steal?

Scenario

Victim’s browseron shop.example.com
shop.example.com
Attackerevil.example
review: <script>…</script>
Step 1 of 5
1

The attacker posts a product review containing a script tag. The site saves it without escaping.

  • Without HttpOnly, the script reads document.cookie and sends the session ID to the attacker.
  • With HttpOnly, the cookie is invisible to the script, so it cannot be stolen, although the script can still act on the page while it is open.
  • A token in localStorage is always readable by any script on the page.
  • The real fix is preventing XSS: escape all user content, avoid inserting raw HTML, and add a Content Security Policy (CSP).

#CSRF (cross-site request forgery)

The attacker cannot read your cookies, but they do not need to. A page on their site makes your browser send a request to your bank, and the browser attaches the bank’s cookie automatically.

CSRF: making your browser act for the attacker

The attacker never sees your cookie. They only need your browser to send it.

Scenario

Victim’s browserbank sid=abc
evil.exampleattacker’s page
bank.examplevictim is logged in
Set-Cookie: sid=abc; SameSite=None; Secure
Step 1 of 5
1

You logged in to your bank earlier. Its cookie allows cross-site sending (SameSite=None).

Defences, used together:

  1. SameSite=Lax or Strict on session cookies, so cross-site POSTs carry no cookie.
  2. CSRF tokens: a random value in every form that the attacker’s page cannot read.
  3. Check the Origin header on state-changing requests.
  4. Never change data with GET, because Lax still sends cookies on top-level GET navigation.

#Where should the browser keep the auth token?

StorageReadable by JavaScript (XSS)Sent automatically (CSRF)Verdict
Cookie with HttpOnly; Secure; SameSite=LaxNoYes, but limited by SameSiteRecommended for websites
localStorageYesNoConvenient, but one XSS bug leaks every token
sessionStorageYesNoSame as localStorage, cleared when the tab closes
JavaScript memory onlyYes, while the page runsNoLost on reload; pair with a refresh-token cookie

Mobile apps do not have cookies’ automatic behaviour; they keep tokens in the operating system’s secure storage (Keychain on iOS, Keystore on Android) and send them in the Authorization header.

#When stateful is the right answer: WebSocket

Some features are stateful by nature. A chat room, a multiplayer game or a live dashboard needs the server to push data the moment it happens. A WebSocket starts as an HTTP request with Upgrade: websocket, the server answers 101 Switching Protocols, and from then on one long-lived TCP connection carries messages in both directions. The server remembers each connection: who it is, which rooms it joined.

WebSocket: when the connection itself is the state

Chat rooms and multiplayer games need the server to push messages, so they keep a long-lived connection open.

Scenario

Chat app
Load balancer
Chat server 1
Chat server 2
Redis pub/sub
GET /chat · Upgrade: websocket
Step 1 of 5
1

A WebSocket starts life as an ordinary HTTP request asking to upgrade the connection.

That is fine, as long as you design for it:

  • The connection is pinned to one server, so the load balancer must support long-lived connections.
  • Users on different servers need a shared pub/sub channel (Redis, Kafka) to reach each other.
  • Servers die, so clients reconnect with backoff, authenticate again, and re-send what the server needs to know. Connection state should be a cache that can always be rebuilt from durable storage.

#State in system design: a decision guide

  • Default to stateless application servers. It makes load balancing, autoscaling, deploys and crash recovery simple.
  • Browser website with login: session ID in an HttpOnly; Secure; SameSite=Lax cookie, session data in Redis with a TTL. Easy logout, small cookies.
  • Public API, mobile app, microservices: short-lived JWT access tokens plus revocable refresh tokens. API keys for machine clients.
  • Real-time features: WebSocket servers are stateful on purpose; share messages through pub/sub and make reconnects cheap.
  • Sticky sessions: acceptable for WebSockets or as a temporary fix, never as the main session strategy.
  • Always: HTTPS everywhere, Secure + HttpOnly + SameSite on auth cookies, session expiry, server-side logout, and XSS prevention.

#Try it yourself

# 1. Ask a test server to set a cookie, and save the cookie jar to a file
curl -c jar.txt https://httpbin.org/cookies/set?session_id=xyz987
cat jar.txt

# 2. Send the stored cookies back automatically, like a browser would
curl -b jar.txt https://httpbin.org/cookies

# 3. See the raw Set-Cookie header in a real login-less response
curl -sI https://www.google.com/ | grep -i set-cookie

# 4. Decode a JWT payload (the middle part) to see that it is readable by anyone
echo 'eyJzdWIiOiI0MiIsInJvbGUiOiJjdXN0b21lciJ9' | base64 -d

# 5. A session in Redis with a 30-minute TTL (if you have redis-cli)
redis-cli SET session:xyz987 '{"userId":42}' EX 1800
redis-cli TTL session:xyz987

In the browser, open developer tools (F12) → Application (Chrome) or Storage (Firefox) → Cookies. You will see every cookie for the site with its Domain, Path, Expires, HttpOnly, Secure and SameSite columns. Log out of a site and watch which cookies disappear. Then try document.cookie in the console: HttpOnly cookies are missing from the result.

#Summary

Key takeaways

  • HTTP is stateless: each request is independent. Applications remember users by keeping state on the server (sessions), in a shared store, or in the client (tokens).
  • Stateful servers keep sessions in RAM: fast and simple, but they break horizontal scaling, consume memory, and lose everything on a restart. Stateless servers let any copy handle any request: scalable and fault-tolerant, at the cost of sending and verifying a token each time.
  • With several servers, fix in-memory sessions with a central store (Redis) or stateless tokens; sticky sessions cause uneven load and lose users when a server dies.
  • Cookies are small (~4 KB) text values set with Set-Cookie and sent back automatically. Session vs persistent depends on Expires/Max-Age; scope with Domain and Path; protect with Secure, HttpOnly and SameSite (Lax by default).
  • Sessions keep the data on the server and give the browser only a random session ID. Expire them, rotate the ID at login, and delete them on logout.
  • JWT = header.payload.signature: readable by anyone, unforgeable without the key, hard to revoke. Use short-lived access tokens with revocable refresh tokens.
  • XSS steals readable tokens (use HttpOnly cookies, escaping, CSP); CSRF abuses automatic cookies (use SameSite, CSRF tokens, Origin checks, safe GETs). WebSockets are stateful by nature: share via pub/sub and design for reconnects.