5 min read
Web Backend Fundamentals Index
Tier 1 -- Core Web & API Basics
Authentication & Authorization
Tier 2 -- Communication Models
Tier 3 -- Production & API Hardening
Web Backend Fundamentals Index
Tier 1 -- Core Web & API Basics
Authentication & Authorization
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.
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?
2. How It Works in Practice
Cookie Flow
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
- User logs in.
- Server creates a record in Redis/DB:
{ session_id: "abc", user_id: 42, roles: ["admin"] }. - Server returns
Set-: session_id=abc. - On every subsequent request, the browser sends
: session_id=abc. - Server fetches the record and treats the user as authenticated.
Cookie Attributes
| Attribute | What It Does |
|---|---|
HttpOnly | JavaScript cannot read the cookie — blocks XSS token theft |
Secure | Cookie sent only over HTTPS |
SameSite=Strict | Cookie not sent on cross-site requests — blocks CSRF |
SameSite=Lax | Sent on top-level navigations only (safe default) |
SameSite=None; Secure | Always sent cross-site (required for embedded widgets) |
Expires / Max-Age | Session (tab-lifetime) vs. persistent cookie |
Domain | Which domain/subdomains receive the cookie |
Path | Limits cookie to a URL path |
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?
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).
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?
4. Trade-offs and Common Mistakes
Trade-offs
| Cookie/Session | Token (JWT) | |
|---|---|---|
| State location | Server | Client |
| Logout | Easy — delete server record | Hard — token valid until expiry |
| Scale | Requires shared store | Stateless, no shared store |
| Revocation | Instant | Requires 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
HttpOnlyto prevent theft,Secureto enforce , andSameSiteto 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})
Reading a cookie in a browser request (for debugging)
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.
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 }); }); });
Glossary History
Click dotted jargon to save explanations here.
Glossary History
Click dotted jargon to save explanations here.