Session-based auth vs. token-based (JWT) auth — what are the real tradeoffs?
Sessions keep state on the server and can be revoked instantly; JWTs keep state on the client and scale beautifully until you need to take one back.
LEARN BY DOINGPut your intuition to work3 steps
Session-based auth vs. token-based (JWT) auth — what are the real tradeoffs?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#Two different philosophies
Session-based and token-based authentication solve the same problem — "remember that this request belongs to an already-logged-in user" — with opposite architectures. A session is stateful: the server creates a record the moment you log in, and every subsequent request is just a lookup against that record. A JWT is stateless: the server issues a self-contained, signed credential and then forgets about it entirely; every subsequent request is verified by checking a signature, not by looking anything up. That one architectural choice cascades into almost every tradeoff below.
#Session-based auth, end to end
// server.js — session-based auth with express-session + Redis storeconst express = require('express');const session = require('express-session');const RedisStore = require('connect-redis').default;const { createClient } = require('redis');const bcrypt = require('bcrypt');const app = express();app.use(express.json());const redisClient = createClient({ url: process.env.REDIS_URL });redisClient.connect();app.use(session({store: new RedisStore({ client: redisClient, prefix: 'sess:' }),secret: process.env.SESSION_SECRET,resave: false,saveUninitialized: false,cookie: {httpOnly: true,secure: true, // HTTPS onlysameSite: 'lax',maxAge: 1000 * 60 * 30, // 30 minutes},}));app.post('/login', async (req, res) => {const { email, password } = req.body;const user = await findUserByEmail(email);if (!user || !(await bcrypt.compare(password, user.passwordHash))) {return res.status(401).json({ error: 'Invalid credentials' });}req.session.userId = user.id; // the server now owns this sessionres.json({ ok: true });});app.post('/logout', (req, res) => {req.session.destroy(() => res.json({ ok: true })); // instant, real revocation});app.get('/me', requireSession, async (req, res) => {const user = await findUserById(req.session.userId);res.json({ id: user.id, email: user.email });});function requireSession(req, res, next) {if (!req.session.userId) return res.status(401).json({ error: 'Not authenticated' });next();}
The client holds nothing but an opaque cookie value — it has no idea what's inside the session, and doesn't need to. All the interesting data (user ID, roles, cart contents, whatever) lives server-side, keyed by that opaque ID.
#Token-based (JWT) auth, end to end
// server.js — token-based auth with a signed JWTconst express = require('express');const jwt = require('jsonwebtoken');const bcrypt = require('bcrypt');const app = express();app.use(express.json());const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET;app.post('/login', async (req, res) => {const { email, password } = req.body;const user = await findUserByEmail(email);if (!user || !(await bcrypt.compare(password, user.passwordHash))) {return res.status(401).json({ error: 'Invalid credentials' });}const token = jwt.sign({ sub: user.id, role: user.role },ACCESS_SECRET,{ algorithm: 'HS256', expiresIn: '15m' });res.json({ token }); // the server hands this out and forgets about it});app.get('/me', requireToken, async (req, res) => {const user = await findUserById(req.auth.sub);res.json({ id: user.id, email: user.email });});function requireToken(req, res, next) {const header = req.headers.authorization || '';const [scheme, token] = header.split(' ');if (scheme !== 'Bearer' || !token) {return res.status(401).json({ error: 'Missing bearer token' });}try {req.auth = jwt.verify(token, ACCESS_SECRET, { algorithms: ['HS256'] });next();} catch {return res.status(401).json({ error: 'Invalid or expired token' });}}// There is no /logout that actually revokes anything — the token stays// valid until it expires no matter what the server does afterward.
#The tradeoffs, side by side
| Dimension | Session-based | Token-based (JWT) |
|---|---|---|
| Where state lives | Server (Redis/DB) + an opaque cookie ID on the client | Entirely on the client; server only holds the signing secret/key |
| Revocation | Instant — delete the session record | Hard by design — valid until exp unless you build a blocklist |
| Horizontal scaling | Needs a shared session store or sticky sessions | Any server holding the secret/public key can verify independently |
| Typical transport | HttpOnly cookie, attached automatically by the browser | Authorization header, attached manually by client code |
| CSRF exposure | Higher — cookies ride along automatically, needs CSRF tokens or SameSite | Lower — nothing is attached automatically |
| XSS exposure | Lower, if the cookie is HttpOnly | Higher if stored in localStorage/sessionStorage — any injected script can read and exfiltrate it |
| Payload overhead per request | Small (a cookie ID) | Larger — claims travel with every single request |
| Mobile / cross-domain / microservices | Awkward — cookie jars and CORS make this painful | A natural fit — an opaque header value, no domain coupling |
| Best default for | Traditional server-rendered web apps, single origin | Public APIs, mobile clients, service-to-service auth |
Neither model is "more secure" in the abstract — session cookies trade CSRF risk for easy revocation, JWTs trade revocation difficulty for statelessness and portability. The right choice depends on what you're building: a single-origin web app with a traditional backend leans session-based; a public API serving native mobile apps and third-party integrations leans token-based; most real systems, as the tip above says, end up doing a bit of both.
🧠 Check your understanding
0/3 · 0/3 answered1. Why is instant, forced logout ('kill this session right now') hard to achieve with a pure JWT design?
2. Which factor pushes a system toward token-based auth over session-based auth?
3. In the hybrid pattern described in the lesson, what is the refresh token typically used for?