Session Store
Storing user sessions in Redis gives you sub-millisecond access, automatic expiry, shared state across replicas, and easy invalidation — without the SQL overhead of a database table. The trade-offs are durability and the need to design for both eviction and security.
Server, expiry, security, scaling
EXAMPLE
// 1) Why Redis for sessions
// • Fast reads/writes (<1 ms)
// • Easy TTL = expiry
// • Shared across web replicas (no sticky sessions required)
// • Cheap to evict / invalidate / rotate
//
// Use a SEPARATE Redis cluster from your cache when sessions matter for compliance:
// • Eviction policies differ (sessions: noeviction or volatile-ttl; cache: allkeys-lru)
// • Backup + persistence requirements differ
// 2) Express + connect-redis (Node)
// npm install express express-session connect-redis redis
import session from 'express-session';
import { RedisStore } from 'connect-redis';
import { createClient } from 'redis';
import crypto from 'node:crypto';
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
const app = express();
app.use(session({
store: new RedisStore({ client: redis, prefix: 'sess:' }),
secret: process.env.SESSION_SECRET, // sign the session cookie
name: 'sid',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 1000 * 60 * 60 * 24 * 7, // 7 days
path: '/',
},
rolling: true, // refresh expiry on every request
}));
// 3) Save data
app.post('/login', async (req, res) => {
const user = await verifyCredentials(req.body.email, req.body.password);
if (!user) return res.sendStatus(401);
// ⚠ regenerate to prevent session fixation
req.session.regenerate((err) => {
if (err) return res.sendStatus(500);
req.session.userId = user.id;
req.session.role = user.role;
res.json({ ok: true });
});
});
app.get('/me', (req, res) => {
if (!req.session.userId) return res.sendStatus(401);
res.json({ userId: req.session.userId, role: req.session.role });
});
// Logout
app.post('/logout', (req, res) => {
req.session.destroy(() => res.clearCookie('sid').sendStatus(204));
});
// 4) The key in Redis
// sess:<sessionId> → JSON blob of req.session data
// TTL = cookie maxAge (auto-extends with 'rolling: true')
// 5) Stateless JWT vs session — pick wisely
// JWT — self-contained; no server lookup; revocation HARDER
// Session in Redis — server lookup per request; trivial revocation; cheap to rotate; private payload
// For browser apps + sensitive data: prefer Redis sessions.
// For machine-to-machine: JWTs work well + a short TTL.
// 6) Session security checklist
// • httpOnly cookie — XSS can't read sid
// • secure (HTTPS only)
// • sameSite = 'lax' or 'strict' — CSRF prevention
// • signed by a high-entropy secret rotated periodically
// • regenerate sid on login + privilege change (session fixation defense)
// • destroy session on logout (don't just clear cookie)
// • idle TTL + absolute max lifetime (force re-auth after N hours)
// • per-user concurrent session limits (optional but recommended for sensitive apps)
// 7) Per-user session list
async function trackSession(userId, sid) {
await redis.sAdd(`user:${userId}:sessions`, sid);
await redis.expire(`user:${userId}:sessions`, 86400 * 30);
}
async function revokeAllSessionsFor(userId) {
const sids = await redis.sMembers(`user:${userId}:sessions`);
if (sids.length) {
await redis.del(sids.map((s) => `sess:${s}`));
await redis.del(`user:${userId}:sessions`);
}
}
// Useful for 'log out of all devices' UX.
// 8) Rolling vs absolute expiry
// rolling: true — extend each request; user stays logged in while active
// absolute — fixed expiry from login; forces re-auth after a strict window
// Hybrid: short idle TTL + long absolute via two timestamps in the session
// 9) Avoiding the dogpile
// On expiry, multiple in-flight requests fail. Either:
// • Refresh slightly BEFORE expiry (90% of TTL)
// • Use a refresh token alongside
// • Send a 401 + ask client to refresh + retry
// 10) Connection + cluster considerations
// • Use Redis Cluster or Sentinel for HA
// • Failover: client library reconnects + re-reads session
// • Pipelining: connect-redis groups operations
// • Per-region replication for multi-region apps (read replicas in each region)
// 11) Persistence + memory policy
// • appendonly yes — AOF for durability
// • maxmemory-policy noeviction OR volatile-ttl
// - noeviction: refuses writes when memory full — sessions can't be lost
// - volatile-ttl: evicts keys nearest expiry — graceful degradation
// • Memory budget: estimate avg session size × max concurrent users
// • Compress big session blobs (zlib) if storing tokens / preferences in session
// 12) Monitoring + alerting
// • redis memory usage
// • session creation / destruction rate
// • TTL distribution (P50, P95)
// • count of sessions per user (anomaly = compromised account?)
// • failed authentication rate paired with session lookup misses
// 13) Other frameworks
// Django: django-redis-sessions; settings.SESSION_ENGINE = 'redis_sessions.session'
// Flask: Flask-Session with type='redis'
// Rails: redis-store gem + session_store :redis_store
// Laravel: SESSION_DRIVER=redis in .env
// Go: github.com/gorilla/sessions + redistore.NewRediStore(...)
// 14) Common bugs
// • secret too short or static across environments — rotate per env, store in vault
// • saveUninitialized: true → every visitor gets a session row; explosion of empty sessions
// • No regenerate on login → session fixation
// • Cookie httpOnly=false → XSS can steal sid
// • SameSite=None without secure → browsers reject
// • TTL longer than required by compliance — sessions outlive consent
// • Storing huge data in session (entire user profile) — keep it tiny; lookup richer data from DB
// • Eviction with allkeys-lru on the session store → random user logouts under memory pressure
// • One Redis for cache AND sessions on same instance — cache evictions silently log users out
Why it matters
Redis sessions give you sub-ms reads, automatic expiry, and shared state across app replicas. Pair connect-redis with HttpOnly + Secure + SameSite cookies, regenerate sids on login to defeat fixation, and run sessions on a separate Redis (with noeviction or volatile-ttl) so a busy cache can’t silently log everyone out.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
SETEX session:abc 1800 $json # 30-min sessions GET session:abc DEL session:abcTry it Yourself »
Discussion
Loading…