This is the interview question. “Sessions or JWT, which would you use?” The honest answer is “depends” — but you better know why. Both solve the same problem: keeping a user logged in across requests. They just take opposite philosophies.
In simple language: sessions are like a coat check at a restaurant — you get a ticket number, the actual coat (your identity) lives with the host. JWT is like a wristband at a festival — everything you need is on the band itself, no central list.
Side-by-side
↓
Server looks up
Redis[abc123] = { userId: 42 }
↓
req.user = ...
↓
Server verifies signature
payload = { userId: 42 }
↓
req.user = ...
Sessions with express-session + Redis
npm install express-session connect-redis ioredis
import session from "express-session";
import { RedisStore } from "connect-redis";
import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL);
app.use(session({
store: new RedisStore({ client: redis }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: true, // HTTPS only
sameSite: "lax",
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
},
}));
app.post("/login", async (req, res) => {
const user = await verifyCredentials(req.body);
req.session.userId = user.id; // server stores this in Redis
res.json({ ok: true }); // browser gets a sid cookie
});
app.get("/me", (req, res) => {
if (!req.session.userId) return res.status(401).end();
res.json({ userId: req.session.userId });
});
app.post("/logout", (req, res) => {
req.session.destroy(() => res.json({ ok: true })); // gone from Redis
});
What’s happening: the cookie holds an opaque session ID. The actual user data lives in Redis. Every request, Express pulls from Redis and hydrates req.session.
JWT with jsonwebtoken
npm install jsonwebtoken
import jwt from "jsonwebtoken";
app.post("/login", async (req, res) => {
const user = await verifyCredentials(req.body);
const token = jwt.sign(
{ sub: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: "1h" }
);
res.json({ token });
});
app.get("/me", (req, res) => {
const token = req.headers.authorization?.slice(7);
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
res.json({ userId: payload.sub });
} catch {
res.status(401).end();
}
});
No Redis. No DB lookup. The token IS the proof.
A decoded payload looks like:
{
"sub": "user_42",
"role": "admin",
"iat": 1716700000,
"exp": 1716703600
}
The tradeoffs
| Concern | Sessions | JWT |
|---|---|---|
| State | Server (Redis/DB) | Client (the token) |
| Revocation | Trivial — delete the key | Hard — token is valid till expiry |
| Horizontal scaling | Need shared store | Free, no shared state |
| Token size | Small (just sid) | Larger (encoded JSON, ~300-1000 bytes) |
| Mobile apps | Cookies are awkward | Bearer tokens are clean |
| CSRF | Vulnerable (cookies auto-sent) | Safer (manual Authorization header) |
| XSS | httpOnly cookies protect it | localStorage tokens are exposed |
| Logout-everywhere | Delete sessions for user | Need token blocklist or rotate secret |
The honest verdict
- Server-rendered web app, same domain? Sessions. Cookies just work, revocation is free.
- Mobile + web + third-party API consumers? JWT, often with short access + long refresh tokens.
- Microservices? JWT, so service B doesn’t need to call the auth service on every request.
- Banking-grade app where you must instantly revoke? Sessions, or JWT with a Redis-backed blocklist (which… is basically sessions again).
The hybrid (what most production apps actually do)
Use JWT access tokens (15 min) for stateless API calls, plus a refresh token stored server-side. Best of both: stateless requests, but revocation possible via the refresh token store. Covered in the Authentication Patterns note.
Common interview follow-ups
- “Where do you store the JWT on the client?” —
httpOnlycookie if same-site, in-memory for SPAs. Avoid localStorage if you can — XSS reads it. - “How do you log a user out of all devices?” — sessions: delete all their sessions in Redis. JWT: rotate the user’s signing secret or maintain a
jtiblocklist. - “Is JWT inherently less secure?” — no, but it’s easier to misuse. People put PII in payloads (it’s just base64, not encrypted), forget to set
expiresIn, or usenonealgorithm.