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.

Beginner

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

  1. Browser logs in: POST /login with email + password.
  2. Server authenticates: hashes the password, compares, verifies alice.
  3. 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
    }
  4. Server sends cookie: tells the browser "here's a session ID":
    Set-Cookie: session_id=<session_id>; Path=/; HttpOnly; Secure; SameSite=Strict
    
  5. Browser stores cookie: browser automatically stores the session_id cookie.
  6. Browser makes requests: on every subsequent request, the browser automatically sends the cookie:
    GET /profile
    Cookie: session_id=<session_id>
    
  7. 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
  8. Browser logs out: POST /logout.
  9. Server deletes session: del sessions[session_id].
  10. 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 day

Without 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?