Sessions vs JWT

intermediate express auth sessions jwt redis

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

Sessions (Stateful)
Server holds the truth
Cookie: sid=abc123

Server looks up
Redis[abc123] = { userId: 42 }

req.user = ...
+ Easy to revoke (delete key)
- Needs shared store (Redis)
JWT (Stateless)
Token carries the truth
Header: Bearer eyJ...

Server verifies signature
payload = { userId: 42 }

req.user = ...
+ No DB hit, scales horizontally
- Can't revoke without extra work

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

ConcernSessionsJWT
StateServer (Redis/DB)Client (the token)
RevocationTrivial — delete the keyHard — token is valid till expiry
Horizontal scalingNeed shared storeFree, no shared state
Token sizeSmall (just sid)Larger (encoded JSON, ~300-1000 bytes)
Mobile appsCookies are awkwardBearer tokens are clean
CSRFVulnerable (cookies auto-sent)Safer (manual Authorization header)
XSShttpOnly cookies protect itlocalStorage tokens are exposed
Logout-everywhereDelete sessions for userNeed 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?”httpOnly cookie 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 jti blocklist.
  • “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 use none algorithm.