1. What It Is

is stateless — each request carries no memory of previous ones. Cookies and sessions are the two foundational mechanisms backends use to maintain state across requests.

: A small key-value string the server sends to the browser in a Set- response header. The browser automatically attaches it to every subsequent request to the same domain. Cookies live in the browser.

: Server-side storage (memory, database, Redis) that holds user state. The server gives the browser a ID (usually via a cookie), and looks up the state on every request using that ID.

Why it matters: almost every web application that requires login uses one or both. Understanding how they work — and where they fail — is essential for backend engineers.


Quick check

A user logs into a web application. On the next request, the server needs to know who the user is. If the application uses sessions, what does the browser actually send to the server to enable this lookup?

Choose one answer

2. How It Works in Practice

Client                          Server
  |                               |
  |-- POST /login --------------->|
  |                               | validates credentials
  |<-- 200 OK --------------------|
  |    Set-Cookie: session_id=abc; HttpOnly; Secure; SameSite=Lax
  |                               |
  |-- GET /dashboard ------------>|
  |   Cookie: session_id=abc      | looks up session state
  |<-- 200 OK --------------------|

Session Flow

  1. User logs in.
  2. Server creates a record in Redis/DB: { session_id: "abc", user_id: 42, roles: ["admin"] }.
  3. Server returns Set-: session_id=abc.
  4. On every subsequent request, the browser sends : session_id=abc.
  5. Server fetches the record and treats the user as authenticated.
AttributeWhat It Does
HttpOnlyJavaScript cannot read the cookie — blocks XSS token theft
SecureCookie sent only over HTTPS
SameSite=StrictCookie not sent on cross-site requests — blocks CSRF
SameSite=LaxSent on same-site requests and on cross-site top-level navigations with a safe method like GET (safe default)
SameSite=None; SecureAlways sent cross-site (required for embedded widgets)
Expires / Max-AgeSession (until the browser closes) vs. persistent cookie
DomainWhich domain/subdomains receive the cookie
PathLimits cookie to a URL path

The cookie carries an ID, the store keeps the state

The browser receives a session ID after login and sends it on the next request. The server uses that ID to fetch the user state from Redis or a database.

The browser sends a login request so the server can validate the credentials.

Text version
  1. The browser sends a login request so the server can validate the credentials.
  2. The server stores user forty-two and the admin role under session ID abc.
  3. The cookie carries only the session ID because the user state stays on the server.
  4. The browser automatically sends the session cookie when it requests the dashboard.
  5. The server uses the cookie's ID to fetch the matching session record.
  6. The returned session record identifies the user, so the server treats the request as authenticated.
Quick check

A web app needs to protect its session cookie from being stolen via an XSS vulnerability where injected JavaScript tries to read document.cookie. Which cookie attribute directly prevents this attack?

Choose one answer

3. Common Real-World Usage

Traditional web apps (server-rendered): server with an opaque ID is the standard. Rails uses ActionDispatch::Session, Django uses the django.contrib.sessions framework, Express uses express-session.

APIs + SPAs: JWTs stored in memory or HttpOnly cookies are more common than server sessions because they are stateless and scale without a shared session store.

"Remember Me": persistent with Max-Age set far in future; the session record gets a long TTL in Redis.

Session stores: Redis is the most popular because it is fast, supports TTL natively, and allows horizontal scaling (any server can read the same session data).


Quick check

Your team is building a REST API consumed by a single-page application that will be deployed across multiple server instances behind a load balancer. A colleague suggests using server-side sessions stored in each server's local memory. What is the primary problem with this approach, and what is the most common solution?

Choose one answer

4. Trade-offs and Common Mistakes

Trade-offs

Cookie/SessionToken (JWT)
State locationServerClient
LogoutEasy — delete server recordHard — token valid until expiry
ScaleRequires shared storeStateless, no shared store
RevocationInstantRequires token blacklist or short expiry

Common Mistakes

Storing IDs in localStorage: JavaScript-accessible storage is trivially read by . Use HttpOnly cookies instead.

Missing SameSite: browser defaults and edge cases have changed over time, so relying on them is brittle. Set SameSite=Lax or Strict explicitly unless you truly need cross-site cookies.

Storing sensitive data in the body: The travels over the wire. Only store a random, opaque ID. Keep sensitive data server-side.

Not rotating IDs on login: If an attacker captures a pre-login session ID (session fixation), they hijack the session. Regenerate the session ID on every privilege change.

Infinite session TTL: Sessions should expire. Use Max-Age and enforce server-side expiry.

Using the same cookie across environments: dev cookies leaking into prod or vice versa.


Server sessions trade shared storage for easy revocation

Server sessions use a shared store when servers scale out and revoke access at once. Stateless JWTs need no shared store but stay valid until expiry.

A session keeps user state on the server, while a JWT carries state on the client.

Text version
  1. A session keeps user state on the server, while a JWT carries state on the client.
  2. Deleting the session record logs the user out, while a stateless JWT remains valid until expiry.
  3. Scaled-out servers read sessions from one shared store, while stateless JWTs need no shared session store.
  4. Deleting a session revokes access at once, while revoking a JWT early needs a denylist that the server checks on each request.

5. Interview Angle

How to explain it

" is stateless. Cookies let the browser carry a small piece of state — usually just a ID — on every request. The server maps that ID to a record held in a store like Redis. The key security controls are HttpOnly to prevent theft, Secure to enforce , and SameSite to prevent ."

Common interview questions

Q: What is the difference between a and a session? A: A is client-side storage (browser). A session is server-side storage. They work together: the server creates a session, gives the browser a session-ID cookie, and looks up the session on each request.

Q: How do you prevent from stealing session tokens? A: Set HttpOnly on the session cookie so JavaScript cannot read it.

Q: How do you prevent ? A: SameSite=Strict/Lax stops the browser from sending the cookie on cross-site requests (Lax still sends it on top-level GET navigations). It is defense in depth, so also use a CSRF token.

Q: What happens when you scale horizontally with server sessions? A: All servers must read from the same session store. Move sessions to Redis or a database rather than in-process memory.

Q: Cookie vs. for auth? A: Sessions are easier to revoke and don't expose user data client-side. JWTs are stateless and simpler to scale. For most apps with revocation requirements (logout, ban), sessions win. For microservices or APIs without login/logout, JWTs make sense.


Three cookie flags guard the session ID

HttpOnly hides the session cookie from JavaScript, Secure keeps it on HTTPS, and SameSite=Lax keeps it off cross-site POST requests.

Without HttpOnly, any script on the page can read the session cookie, so an XSS bug can steal it.

Text version
  1. Without HttpOnly, any script on the page can read the session cookie, so an XSS bug can steal it.
  2. Secure tells the browser to send the cookie only over HTTPS, never over plain HTTP.
  3. SameSite set to Lax limits cross-site sends, so a forged POST from another site arrives without the session cookie.

6. Practical Examples

Express.js server session with Redis

const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');

const redisClient = createClient();
await redisClient.connect();

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 1000 * 60 * 60 * 24, // 24 hours
  },
}));

app.post('/login', async (req, res) => {
  const user = await authenticate(req.body);
  req.session.regenerate(() => {       // prevent session fixation
    req.session.userId = user.id;
    res.json({ ok: true });
  });
});

Django session (built-in)

# settings.py
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'default'  # points to Redis
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_SAMESITE = 'Lax'

# view
def login_view(request):
    user = authenticate(request, username=request.POST['username'], password=request.POST['password'])
    if user is None:
        return JsonResponse({'ok': False}, status=401)
    request.session.cycle_key()          # regenerate session ID
    request.session['user_id'] = user.id
    return JsonResponse({'ok': True})
GET /api/me HTTP/1.1
Host: api.example.com
Cookie: session_id=abc123xyz

The ID is opaque — it reveals nothing about the user. All sensitive data stays on the server.

Regenerate the session ID on login

An attacker may know a pre-login session ID. Regenerating the ID on login prevents that ID from becoming the authenticated session ID.

An attacker plants a session ID before login, so they already know the ID the browser sends.

Text version
  1. An attacker plants a session ID before login, so they already know the ID the browser sends.
  2. If login keeps that old ID, the attacker can use it to hijack the authenticated session.
  3. After checking credentials, Express regenerates the session ID before storing the user ID, while Django calls cycle key.
  4. The browser receives a new session ID, so the attacker cannot use the old ID to access the authenticated session.
Quick check

In the Express.js login handler shown below, why is req.session.regenerate() called immediately after a user successfully authenticates? app.post('/login', async (req, res) => { const user = await authenticate(req.body); req.session.regenerate(() => { req.session.userId = user.id; res.json({ ok: true }); }); });

Choose one answer

Reading

On this page

My notes

Sign in to keep highlights and notes.

Glossary history

Click dotted jargon to save explanations here.