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.
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.
| Feature | Stateful server | Stateless server |
|---|---|---|
| Where the state lives | In server memory | In the token the client sends (or a shared store) |
| Scalability | Hard and complicated to scale | Very easy (ideal for microservices) |
| Server memory | Grows with the number of clients | No extra load per client |
| Server crash or deploy | Sessions on it are lost | Nothing is lost |
| Load balancing | Needs sticky sessions or a shared store | Any server, any request |
| Per-request cost | Small ID; fast memory lookup | Larger token; signature check |
| Logout / revoke access | Easy: delete the session | Hard: token valid until it expires |
| Typical uses | Real-time multiplayer games, chat rooms, WebSocket connections | REST 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 logs in. The load balancer happens to send him to Server A, which stores his session in its own RAM.
| Approach | How | Pros | Cons |
|---|---|---|---|
| Sticky sessions (session affinity) | The load balancer always sends a user to the same server, using a cookie or the client’s IP | No code change; fast | Uneven load; a server crash or deploy logs its users out; adding servers does not help existing users |
| Central session store | Sessions live in Redis (or a database) shared by all servers | App servers become stateless; easy logout and revocation | One 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 client | No lookup, no shared store; scales anywhere | Hard 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 aCookieheader.
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
#Types of cookie
- Session cookie: has no
ExpiresorMax-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.
#Cookie attributes
| Attribute | What it does | Recommended for a session cookie |
|---|---|---|
Expires / Max-Age | When the cookie dies. Missing = session cookie. Max-Age=0 deletes it | Leave out, or keep short |
Domain | Which hosts receive it. Missing = only the exact host that set it. Domain=example.com includes all subdomains | Leave out (host-only) |
Path | Only sent for URLs under this path | Path=/ |
Secure | Only sent over HTTPS, never plain HTTP | Always |
HttpOnly | JavaScript cannot read it (document.cookie), which stops XSS scripts from stealing it | Always |
SameSite | Whether it is sent on requests started by other sites: Strict, Lax or None | Lax (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
- The user submits the login form (
POST /login). - The server verifies the password and creates a session in its store (for example
session_id: xyz987). - The server responds with
Set-Cookie: session_id=xyz987; HttpOnly. - The browser saves
xyz987as a cookie. - Later, when the user opens the dashboard (
GET /dashboard), the browser sends the headerCookie: session_id=xyz987. - The server looks up
xyz987in 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
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
HS256it is an HMAC using one shared secret; withRS256orES256the 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
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 token | Refresh token | |
|---|---|---|
| Lifetime | Short: 5–15 minutes | Long: days to weeks |
| Sent with | Every API request | Only to the token endpoint |
| Checked by | Signature + expiry, no database | Database lookup, so it can be revoked |
| If stolen | Usable for minutes | Revoke 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
The attacker posts a product review containing a script tag. The site saves it without escaping.
- Without
HttpOnly, the script readsdocument.cookieand 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
localStorageis 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
You logged in to your bank earlier. Its cookie allows cross-site sending (SameSite=None).
Defences, used together:
SameSite=LaxorStricton session cookies, so cross-site POSTs carry no cookie.- CSRF tokens: a random value in every form that the attacker’s page cannot read.
- Check the
Originheader on state-changing requests. - Never change data with GET, because Lax still sends cookies on top-level GET navigation.
#Where should the browser keep the auth token?
| Storage | Readable by JavaScript (XSS) | Sent automatically (CSRF) | Verdict |
|---|---|---|---|
Cookie with HttpOnly; Secure; SameSite=Lax | No | Yes, but limited by SameSite | Recommended for websites |
localStorage | Yes | No | Convenient, but one XSS bug leaks every token |
sessionStorage | Yes | No | Same as localStorage, cleared when the tab closes |
| JavaScript memory only | Yes, while the page runs | No | Lost 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
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=Laxcookie, 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+SameSiteon 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-Cookieand sent back automatically. Session vs persistent depends onExpires/Max-Age; scope withDomainandPath; protect withSecure,HttpOnlyandSameSite(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.
Answer on your own first, then read the explanation, even when you got it right.
Self-checkQuestion
What does it mean that HTTP is stateless?
Self-checkQuestion
About how much data can one cookie hold?
Self-checkQuestion
A server sends Set-Cookie: theme=dark; Path=/ with no Expires and no Max-Age. What kind of cookie is it?
Self-checkQuestion
What are the three parts of a JWT, in order?
Self-checkQuestion
A session cookie is set without any SameSite attribute. How do modern browsers treat it?
Self-checkQuestion
Which header does the server use to store a cookie, and which header does the browser use to send it back?
Self-checkQuestion
Which of these two tokens is checked with a database lookup, so that it can be revoked?
Self-checkQuestion
How does a WebSocket connection begin?
Self-checkQuestion
A user logs in at 09:00 on Monday. The site has a 30-minute idle timeout and a 7-day absolute timeout. The user stays active all week and is never idle for more than 10 minutes. When must they log in again?
Self-checkQuestion
You store session:xyz987 in Redis with an expiry of 1800 seconds. Ten minutes later you run redis-cli TTL session:xyz987. Roughly what number do you see?
Self-checkQuestion
The server sends Set-Cookie: lang=bn; Max-Age=2592000; Path=/. How long does the cookie live?
Self-checkQuestion
A developer puts the user’s phone number and password hash in a JWT payload, saying ‘it is signed, so it is safe’. What is the problem?
Self-checkQuestion
An attacker decodes their own JWT, changes role from customer to admin, encodes it again and sends it. What happens?
Self-checkQuestion
Ten microservices must verify user tokens, but only the auth server may issue them. Which signing setup fits?
Self-checkQuestion
An attacker steals a user’s access token (15-minute lifetime). Seconds later you revoke the user’s refresh tokens. How long can the stolen access token still work?
Self-checkQuestion
Logout only clears the session cookie in the browser. Earlier, an attacker copied that session ID. What happens when the attacker uses it after logout?
Self-checkQuestion
Before the victim logs in, an attacker manages to plant a session ID they know in the victim’s browser. Which practice makes that ID useless?
Self-checkQuestion
A developer uses the user’s numeric ID as the session ID: session_id=42. What is wrong?
Self-checkQuestion
The notes say ‘HttpOnly prevents XSS attacks’. Which statement is correct?
Self-checkQuestion
A page on evil.example auto-submits a form that POSTs to bank.example/transfer. The bank’s session cookie has SameSite=Lax. What happens?
Self-checkQuestion
A bank performs transfers with GET /transfer?to=…&amount=…, and its session cookie uses SameSite=Lax. An attacker posts a link to that URL, and a logged-in user clicks it. Why is this dangerous?
Self-checkQuestion
A single-page app keeps its JWT in localStorage. A stored XSS bug is found in the comments section. What is the consequence?
Self-checkQuestion
A colleague says: ‘Our API is stateless, so we do not need a database.’ What is wrong with this?
Self-checkQuestion
Which statement about API keys is correct?
Self-checkQuestion
In a chat app, user A is connected by WebSocket to server 1 and user B to server 2, in the same room. How does A’s message reach B?
Self-checkQuestion
A payment form is embedded as an iframe on many shops’ websites and needs its own cookie there. Which cookie setting does it need?
Self-checkQuestion
You move sessions from each server’s RAM into a central Redis store. Which new risk must you design for?
Self-checkQuestion
Which two attributes should every session cookie always have? (Choose two.)
Choose 2
Self-checkQuestion
Which two statements about stateless application servers are true? (Choose two.)
Choose 2
Self-checkQuestion
Which two are genuine weaknesses of sticky sessions? (Choose two.)
Choose 2
Self-checkQuestion
Which two statements about JWTs are true? (Choose two.)
Choose 2
Self-checkQuestion
Which two defences directly protect against CSRF? (Choose two.)
Choose 2