Cookies and Sessions

5 min read

Reading Progress0%
Web Backend Fundamentals Index
Tier 1 -- Core Web & API Basics
Tier 2 -- Communication Models
Tier 3 -- Production & API Hardening
Web Backend Fundamentals Index
Tier 1 -- Core Web & API Basics
Tier 2 -- Communication Models
Tier 3 -- Production & API Hardening

Cookies and Sessions

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 top-level navigations only (safe default)
SameSite=None; SecureAlways sent cross-site (required for embedded widgets)
Expires / Max-AgeSession (tab-lifetime) vs. persistent cookie
DomainWhich domain/subdomains receive the cookie
PathLimits cookie to a URL path

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.


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 prevents the browser from sending the cookie on cross-origin requests. For legacy APIs, 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.


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.POST['username'], request.POST['password'])
    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.

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