Sessions and cookies
HTTP is stateless — each request is independent. Sessions fake state by storing data server-side and sending the client a session ID in a cookie. On each request, the client sends the cookie, and the server looks up the session. Simple, but relies on cookies working correctly.
4 min read
The HTTP problem: stateless requests
HTTP is stateless. The server doesn't remember anything about previous requests:
Request 1: GET / (browser: "hello server")
Response: 200 OK (server: "hello browser")
Request 2: GET /profile (browser: "show me my profile")
Response: 200 OK (server: "wait, who are you?")
The server has no memory of Request 1. It doesn't know the browser is logged-in alice. Every request is treated as coming from an anonymous user.
Sessions fake statefulness: the server remembers who is who, and tells the browser a secret identifier to prove its identity.
Session flow
- Browser logs in:
POST /loginwith email + password. - Server authenticates: hashes the password, compares, verifies alice.
- Server creates session: generates a random session ID, stores alice's data in memory or database:
session_id = generate_random_token() sessions[session_id] = { 'user_id': alice.id, 'email': 'alice@example.com', 'created_at': now, 'expires_at': now + 24_hours } - Server sends cookie: tells the browser "here's a session ID":
Set-Cookie: session_id=<session_id>; Path=/; HttpOnly; Secure; SameSite=Strict - Browser stores cookie: browser automatically stores the session_id cookie.
- Browser makes requests: on every subsequent request, the browser automatically sends the cookie:
GET /profile Cookie: session_id=<session_id> - Server reads session: looks up the session ID, finds alice's data, serves her profile:
session_id = request.cookies['session_id'] session = sessions.get(session_id) if session: user_id = session['user_id'] return get_user_profile(user_id) else: return 401 # session expired or invalid - Browser logs out:
POST /logout. - Server deletes session:
del sessions[session_id]. - Browser retains cookie (it won't matter — server won't find the session).
Cookies: HTTP's state machine
A cookie is a small file the browser stores. On every request to the server, the browser automatically includes cookies (if they match the domain/path).
Set-Cookie: session_id=abc123; Domain=myapp.com; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=86400
Flags mean:
- HttpOnly: JavaScript can't read this cookie (prevents XSS attacks from stealing it).
- Secure: only sent over HTTPS (prevents eavesdropping).
- SameSite=Strict: only sent if the request originated from myapp.com (prevents CSRF attacks).
- Max-Age=86400: cookie expires after 86400 seconds (1 day). After that, it's deleted.
- Domain: which domains the cookie is sent to.
- Path: which paths the cookie is sent to (default /).
A well-configured session cookie:
Set-Cookie: session_id=<random_token>; HttpOnly; Secure; SameSite=Strict
Session storage: in-memory vs database
In-memory (simplest for single-server apps):
sessions = {} # in-memory store
# Create session
sessions[session_id] = {'user_id': alice.id, 'expires_at': now + 24_hours}
# Check session
if session_id in sessions:
user_id = sessions[session_id]['user_id']Problems: if the server restarts, all sessions are lost. If the app scales to multiple servers, only one server knows about the session.
Database (for multi-server apps):
# Create session
db.sessions.insert({'session_id': session_id, 'user_id': alice.id, 'expires_at': now + 24_hours})
# Check session
session = db.sessions.find_one({'session_id': session_id, 'expires_at': {'$gt': now}})
if session:
user_id = session['user_id']Database sessions survive server restarts and work across multiple servers.
Session vs stateless (JWT)
Sessions are stateful: the server must maintain a database of sessions.
JWT (JSON Web Token) is an alternative: the client stores encrypted data, and the server verifies the encryption signature:
Claim (client has this data): {"user_id": alice.id, "expires_at": 2025-08-25}
Server signs it: JWT = encode_and_sign(claim, secret_key)
Client stores the JWT and sends it back
Server verifies: does the signature match? If yes, trust the data inside.
Sessions and JWTs are different approaches; each has trade-offs covered in a future lesson. For now: sessions store data server-side and send a reference to the client; JWTs store data client-side and send the whole thing.
Common mistakes
Missing HttpOnly flag
# WRONG — JavaScript can read this cookie
response.set_cookie('session_id', token) # no HttpOnly
# CORRECT
response.set_cookie('session_id', token, httponly=True, secure=True)Without HttpOnly, an XSS vulnerability (injected script) can steal the session cookie.
Missing SameSite flag
# WRONG — sent on cross-site requests (CSRF risk)
response.set_cookie('session_id', token) # no SameSite
# CORRECT
response.set_cookie('session_id', token, samesite='Strict')Without SameSite, an attacker can trick a logged-in user into making unwanted requests.
Not expiring sessions
# WRONG — sessions live forever
response.set_cookie('session_id', token) # no expiration
# CORRECT
response.set_cookie('session_id', token, max_age=86400) # 1 dayWithout expiration, a stolen session ID works forever. Expiration forces re-authentication periodically.
Storing sensitive data in cookies
Cookies are sent with every request and visible in the browser. Don't store credit cards, passwords, or PII in cookies.
# WRONG
response.set_cookie('payment_method', 'visa-1234') # card visible in cookies
# CORRECT
response.set_cookie('session_id', random_token) # token is meaningless without server lookup
# (server stores payment method in database, keyed by session_id)Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Why are sessions needed in a stateless HTTP protocol?
2. What does the HttpOnly cookie flag do?
3. Why should session cookies have an expiration time?
4. When should you use database-backed session storage instead of in-memory?