Suggest an editImprove this articleRefine the answer for “How does session management work in Express.js?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Session management in Express.js** stores user data on the server and sends the client only a signed cookie containing the session ID. On every request, the middleware verifies that ID and retrieves the matching data from the store. ```js app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { httpOnly: true, sameSite: 'lax' } })); ``` **Key point:** the cookie carries only a signed ID, never the actual data. The server controls expiration and revocation by deleting the session record.Shown above the full answer for quick recall.Answer (EN)Image**Session management in Express.js** stores user-specific data on the server across stateless HTTP requests, using a session ID cookie to identify the matching server-side record. ## Theory ### TL;DR - Analogy: a coat check. You hand over your coat (data), get a ticket (cookie), reclaim it later by showing the ticket. - The cookie holds only a signed ID, never the actual data. All data stays server-side. - `connect.sid` carries the session ID; `req.session` gives you the stored object on every request. - Use sessions when the server needs to control state (login, revocation). Use JWT for stateless APIs. - Default MemoryStore works only in development. Redis or Postgres for production. ### Quick example ```js const express = require('express'); const session = require('express-session'); const app = express(); app.use(session({ secret: process.env.SESSION_SECRET, // Signs the session ID cookie resave: false, // Skip write if session unchanged saveUninitialized: false, // Don't create sessions for anonymous users cookie: { maxAge: 60000, httpOnly: true, sameSite: 'lax' } })); app.post('/login', (req, res) => { req.session.userId = '123'; // Data stored on the server res.json({ ok: true }); // Client receives: connect.sid=s%3Aabc.xyz }); app.get('/profile', (req, res) => { if (!req.session.userId) return res.status(401).json({ error: 'Not logged in' }); res.json({ user: req.session.userId }); // Looked up by cookie ID }); ``` The middleware attaches `req.session` on every request. You write to it like a plain object. Express-session handles reading, writing, and signing behind the scenes. ### How session storage actually works When a request arrives, `express-session` reads the `connect.sid` cookie and verifies the HMAC-SHA256 signature against your `secret`. If valid, it fetches the session object from the store (by default, an in-memory JS object). At the end of the request cycle, it writes any changes back to the store. The cookie itself carries only the signed ID. Nothing sensitive. All actual data stays server-side. That is the entire point of the pattern. Node's event-driven request pipeline means store reads and writes happen async, so they don't block other requests. With Redis, this is fast enough for high-traffic apps. ### Key difference from client-side storage localStorage and client-side tokens expose everything to JavaScript and XSS attacks. A session cookie with `httpOnly: true` is not readable by scripts at all. Beyond that, the server controls expiration and revocation by simply deleting the session record. The cookie stays small (just an ID), so there is no payload bloat on every request. ### When to use sessions - User authentication: store login state and roles server-side. Server can invalidate instantly. - Shopping cart: persist items before checkout without exposing data to the client. - A/B testing: track user groups server-side so clients cannot tamper with their assignment. - Avoid for public APIs consumed by SPAs or mobile apps. JWT is a better fit there, no server storage needed. - For clustered deployments or sustained high traffic (above roughly 10 req/s), pair sessions with Redis. MemoryStore won't hold up. ### Session stores The default MemoryStore leaks memory in production and breaks in clustered setups. Each Node process has its own memory, so a login on worker 1 fails on worker 2. This is the most common complaint about sessions on Stack Overflow. | Store | Package | Use when | |---|---|---| | MemoryStore | Built-in | Dev only | | Redis | `connect-redis` | Production, any scale | | PostgreSQL | `connect-pg-simple` | Already on Postgres | | MongoDB | `connect-mongo` | Already on MongoDB | ```js const RedisStore = require('connect-redis').default; const { createClient } = require('redis'); const redisClient = createClient({ url: process.env.REDIS_URL }); await redisClient.connect(); app.use(session({ store: new RedisStore({ client: redisClient }), secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { secure: true, sameSite: 'lax', httpOnly: true } })); ``` ### Sessions vs JWT | | Sessions | JWT | |---|---|---| | Storage | Server-side | Client-side | | Scalability | Needs a shared store | Stateless | | Revocation | Easy (delete the record) | Hard (needs a blacklist) | | Cookie size | Small (ID only) | Larger token | | Best for | Server-rendered apps, admin panels | APIs, SPAs, microservices | The short version: if you need to revoke access immediately (force logout, ban a user), sessions win. If you're building distributed systems or microservices where a shared store isn't practical, JWT is simpler. ### Common mistakes **Hardcoded secret in code** ```js // Wrong - one Git commit exposes this secret: 'keyboard cat' // Right secret: process.env.SESSION_SECRET ``` If this ends up in a public repo, attackers can recalculate the HMAC signature and forge any session cookie. **`saveUninitialized: true`** The old default. This creates a session record for every anonymous visitor and every bot. MemoryStore fills up fast, OOM crashes follow. Set it to `false` and sessions only get created after you write something to `req.session`. **No shared store in a cluster** ```js // Breaks in pm2 cluster mode or Docker with multiple replicas app.use(session({ /* no store option */ })); ``` Each process keeps its own in-memory sessions. User logs in on worker 1, next request hits worker 2, session is gone. Switch to `connect-redis` or `connect-pg-simple`. **`secure: true` in development** Cookies with `secure: true` only travel over HTTPS. They are never sent on localhost HTTP. The fix: ```js cookie: { secure: process.env.NODE_ENV === 'production' } ``` **Missing `req.session.regenerate()` after login** Session fixation: an attacker pre-sets a known `connect.sid` cookie before the victim logs in. After login, both sides share that session. Calling `req.session.regenerate()` right after successful auth issues a new ID and copies the data over. ```js app.post('/login', async (req, res) => { const user = await authenticate(req.body); if (!user) return res.status(401).json({ error: 'Invalid credentials' }); req.session.regenerate(err => { // New ID prevents fixation if (err) return res.status(500).json({ error: 'Session error' }); req.session.userId = user.id; req.session.roles = user.roles; res.json({ ok: true }); }); }); ``` ### Real-world usage - Passport.js: calls `serializeUser` to store only the user ID in `req.session.passport`, then `deserializeUser` to load the full user on each request. - Socket.io: shares the Express session by passing the middleware directly into `io.use()`. - Strapi / KeystoneJS: Redis-backed sessions for admin panel auth. - Next.js custom server: session middleware runs before `next()` to gate SSR routes. ### Follow-up questions **Q:** What is a session fixation attack? **A:** An attacker sets a known `connect.sid` cookie before the victim logs in. After login, both sides share that session. Fix with `req.session.regenerate()` on every successful auth - it issues a new ID and copies the existing data. **Q:** When should you switch from MemoryStore to Redis? **A:** As soon as you leave development or run more than one process. In a cluster, MemoryStore isolates each worker. On a single process with real traffic it leaks memory over time. Redis handles both problems with one change. **Q:** How does `resave: false` improve performance? **A:** It skips the store write if session data did not change during the request. In read-heavy apps this cuts DB or Redis writes by 50-70%. **Q:** How do you revoke sessions across all devices? **A:** Store an `activeSessions` Set in Redis per user ID, with each session ID as a member. On login, add the new session ID. On logout-all, delete every member and the set. On each request, check membership before allowing access. **Q:** `sameSite: 'strict'` vs `'lax'` for CSRF protection? **A:** `'strict'` blocks the cookie on all cross-site requests, including top-level navigation like clicking a link. `'lax'` allows it on safe top-level navigation but blocks cross-site POSTs. Most apps use `'lax'` and pair it with a CSRF token for mutation endpoints. ## Examples ### Basic login, protected route, and logout ```js const express = require('express'); const session = require('express-session'); const app = express(); app.use(express.json()); app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { httpOnly: true, sameSite: 'lax', secure: process.env.NODE_ENV === 'production', maxAge: 24 * 60 * 60 * 1000 // 24 hours } })); app.post('/login', async (req, res) => { const { email, password } = req.body; const user = await db.users.findOne({ email }); if (!user || !await bcrypt.compare(password, user.passwordHash)) { return res.status(401).json({ error: 'Invalid credentials' }); } req.session.regenerate(err => { if (err) return res.status(500).json({ error: 'Session error' }); req.session.userId = user.id; req.session.roles = user.roles; res.json({ ok: true }); }); }); app.get('/me', (req, res) => { if (!req.session.userId) return res.status(401).json({ error: 'Not authenticated' }); res.json({ userId: req.session.userId, roles: req.session.roles }); }); app.post('/logout', (req, res) => { req.session.destroy(err => { if (err) return res.status(500).json({ error: 'Logout failed' }); res.clearCookie('connect.sid'); res.json({ ok: true }); }); }); ``` The key detail is `req.session.regenerate()` before writing user data. Skipping that step is exactly how session fixation attacks succeed in practice. ### Production setup with Redis and reusable auth middleware ```js const RedisStore = require('connect-redis').default; const { createClient } = require('redis'); const redisClient = createClient({ url: process.env.REDIS_URL }); await redisClient.connect(); app.use(session({ store: new RedisStore({ client: redisClient }), secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { secure: true, httpOnly: true, sameSite: 'lax', maxAge: 86400000 } })); function requireAuth(req, res, next) { if (!req.session.userId) return res.status(401).json({ error: 'Not authenticated' }); next(); } function requireRole(role) { return (req, res, next) => { if (!req.session.roles?.includes(role)) { return res.status(403).json({ error: 'Forbidden' }); } next(); }; } app.get('/admin', requireAuth, requireRole('admin'), (req, res) => { res.json({ message: 'Admin panel' }); }); ``` I've seen teams inline `if (!req.session.userId)` in every route handler. That works until you need to change auth logic in 40 places at once. ### Revoking sessions across all devices ```js // On login: register session ID in a per-user Redis Set app.post('/login', async (req, res) => { // ... authenticate user ... req.session.regenerate(async err => { if (err) return res.status(500).json({ error: 'Error' }); req.session.userId = user.id; // Track this session ID under the user's key await redisClient.sAdd(`user:${user.id}:sessions`, req.session.id); res.json({ ok: true }); }); }); // Logout everywhere: delete all sessions for this user app.post('/logout-all', requireAuth, async (req, res) => { const sessionIds = await redisClient.sMembers(`user:${req.session.userId}:sessions`); for (const id of sessionIds) { await redisClient.del(`sess:${id}`); // connect-redis key prefix } await redisClient.del(`user:${req.session.userId}:sessions`); res.json({ ok: true }); }); ``` This answers the classic senior interview question directly. Cookies are ID-based, so clearing cookies on one device removes only that device's ID. Revoking all devices requires server-side tracking like this.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.