Database

Redis in Production: Caching, Sessions, Pub/Sub & Rate Limiting

A
Admin
March 9, 2026 • 6 min read • 1,108 words
8
Overall Scoreout of 10

Rating

8/10

Verdict

Redis is indispensable for production web applications. Use it for caching, sessions, rate limiting, and background job queues. Always configure persistence and use Redis Cluster or Sentinel for high availability.

Pros

  • Sub-millisecond latency for hot data
  • Rich data structures (sorted sets, streams, etc.)
  • Pub/Sub enables real-time features with zero overhead
  • BullMQ makes reliable background jobs easy
  • Redis Cluster provides transparent horizontal scaling

Cons

  • In-memory means data loss risk without AOF/RDB persistence
  • Memory is expensive compared to disk storage
  • Redis Cluster adds operational complexity
  • Not suitable for large datasets that exceed RAM

Redis in Production: Caching, Sessions, Pub/Sub & Rate Limiting

Redis is the Swiss Army knife of backend infrastructure. Starting as a simple key-value store, it has evolved into a multi-model data structure server that powers caching layers, session stores, real-time messaging, rate limiters, leaderboards, and job queues — often all at once in a single application. This guide covers production-grade Redis patterns with Node.js.

Connection Setup with ioredis

// src/lib/redis.ts import Redis from 'ioredis'; const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379'; export const redis = new Redis(REDIS_URL, { maxRetriesPerRequest: 3, enableReadyCheck: true, lazyConnect: true, keepAlive: 30000, connectTimeout: 10000, // Retry with exponential backoff retryStrategy: (times) => { if (times > 10) return null; // Stop retrying after 10 attempts return Math.min(times * 100, 3000); }, }); redis.on('error', (err) => console.error('[Redis] Error:', err.message)); redis.on('connect', () => console.log('[Redis] Connected')); redis.on('reconnecting', () => console.log('[Redis] Reconnecting...')); // Graceful shutdown process.on('SIGTERM', () => redis.quit());

Caching Strategies

Cache-Aside Pattern

// Generic cache wrapper with TTL and serialization async function cached( key: string, ttlSeconds: number, fetchFn: () => Promise ): Promise { const cached = await redis.get(key); if (cached) { return JSON.parse(cached) as T; } const data = await fetchFn(); await redis.setex(key, ttlSeconds, JSON.stringify(data)); return data; } // Usage const user = await cached( `user:${userId}`, 300, // 5 minutes () => prisma.user.findUnique({ where: { id: userId } }) ); // Invalidate cache await redis.del(`user:${userId}`); // Batch invalidation with patterns const keys = await redis.keys(`user:*`); if (keys.length > 0) await redis.del(...keys);

Write-Through Cache

// Update cache and database simultaneously async function updateUser(id: string, data: Partial): Promise { const updated = await prisma.user.update({ where: { id }, data, }); // Immediately update cache await redis.setex(`user:${id}`, 300, JSON.stringify(updated)); return updated; }

Session Storage

// Store sessions in Redis with automatic expiry import { v4 as uuidv4 } from 'uuid'; interface Session { userId: string; email: string; role: string; createdAt: number; } const SESSION_TTL = 7 * 24 * 60 * 60; // 7 days in seconds async function createSession(userId: string, email: string, role: string): Promise { const sessionId = uuidv4(); const session: Session = { userId, email, role, createdAt: Date.now(), }; await redis.setex(`session:${sessionId}`, SESSION_TTL, JSON.stringify(session)); return sessionId; } async function getSession(sessionId: string): Promise { const data = await redis.get(`session:${sessionId}`); return data ? JSON.parse(data) : null; } async function refreshSession(sessionId: string): Promise { await redis.expire(`session:${sessionId}`, SESSION_TTL); } async function destroySession(sessionId: string): Promise { await redis.del(`session:${sessionId}`); } // Destroy all sessions for a user (on password change, etc.) async function destroyAllUserSessions(userId: string): Promise { const keys = await redis.keys('session:*'); const pipeline = redis.pipeline(); for (const key of keys) { pipeline.get(key); } const results = await pipeline.exec(); const toDelete: string[] = []; results?.forEach(([err, value], i) => { if (!err && value) { const session = JSON.parse(value as string) as Session; if (session.userId === userId) { toDelete.push(keys[i]); } } }); if (toDelete.length > 0) { await redis.del(...toDelete); } }

Distributed Rate Limiting

// Sliding window rate limiter using Redis sorted sets async function checkRateLimit( identifier: string, maxRequests: number, windowSeconds: number ): Promise<{ allowed: boolean; remaining: number; resetAt: number }> { const now = Date.now(); const windowStart = now - windowSeconds * 1000; const key = `ratelimit:${identifier}`; const pipeline = redis.pipeline(); pipeline.zremrangebyscore(key, '-inf', windowStart); // Remove old entries pipeline.zadd(key, now, `${now}-${Math.random()}`); // Add current request pipeline.zcard(key); // Count requests in window pipeline.expire(key, windowSeconds); const results = await pipeline.exec(); const requestCount = (results?.[2]?.[1] as number) ?? 0; const allowed = requestCount <= maxRequests; const remaining = Math.max(0, maxRequests - requestCount); const resetAt = now + windowSeconds * 1000; return { allowed, remaining, resetAt }; } // Express middleware export function redisRateLimit(maxRequests: number, windowSeconds: number) { return async (req: Request, res: Response, next: NextFunction) => { const ip = req.headers['x-forwarded-for']?.toString().split(',')[0] || req.ip; const { allowed, remaining, resetAt } = await checkRateLimit( `ip:${ip}`, maxRequests, windowSeconds ); res.setHeader('X-RateLimit-Limit', maxRequests); res.setHeader('X-RateLimit-Remaining', remaining); res.setHeader('X-RateLimit-Reset', Math.ceil(resetAt / 1000)); if (!allowed) { return res.status(429).json({ error: 'Rate limit exceeded', retryAfter: Math.ceil((resetAt - Date.now()) / 1000), }); } next(); }; }

Pub/Sub for Real-Time Features

// Publisher (e.g., when a new comment is posted) const publisher = new Redis(REDIS_URL); async function publishEvent(channel: string, event: object): Promise { await publisher.publish(channel, JSON.stringify(event)); } // Publish new comment event await publishEvent('comments:post:123', { type: 'NEW_COMMENT', comment: { id: '456', content: 'Great post!', authorName: 'Alice' }, postId: '123', timestamp: Date.now(), }); // Subscriber (in a WebSocket handler or separate service) const subscriber = new Redis(REDIS_URL); await subscriber.subscribe('comments:post:123'); subscriber.on('message', (channel: string, message: string) => { const event = JSON.parse(message); console.log(`Received on ${channel}:`, event); // Broadcast to WebSocket clients watching this post wss.clients.forEach(client => { if (client.readyState === WebSocket.OPEN) { client.send(message); } }); }); // Pattern subscribe (all comment channels) await subscriber.psubscribe('comments:*'); subscriber.on('pmessage', (pattern, channel, message) => { console.log(`Pattern ${pattern} | Channel ${channel}`); });

Background Jobs with BullMQ

import { Queue, Worker } from 'bullmq'; // Define job queues export const emailQueue = new Queue('emails', { connection: { url: REDIS_URL }, defaultJobOptions: { attempts: 3, backoff: { type: 'exponential', delay: 2000 }, removeOnComplete: { count: 100 }, removeOnFail: { count: 50 }, }, }); // Add jobs await emailQueue.add('send-welcome', { to: 'user@example.com', name: 'Alice', template: 'welcome', }); // Scheduled job (run every hour) await emailQueue.add( 'send-digest', { type: 'weekly-digest' }, { repeat: { pattern: '0 9 * * MON' } } // Every Monday at 9am ); // Worker to process jobs const emailWorker = new Worker( 'emails', async (job) => { console.log(`Processing job ${job.id}: ${job.name}`); const { to, name, template } = job.data; await sendEmail({ to, template, data: { name } }); return { sent: true, timestamp: Date.now() }; }, { connection: { url: REDIS_URL }, concurrency: 5, // Process 5 jobs simultaneously } ); emailWorker.on('completed', (job) => console.log(`Job ${job.id} completed`)); emailWorker.on('failed', (job, err) => console.error(`Job ${job?.id} failed:`, err));

Sorted Sets: Leaderboards & Analytics

// Real-time leaderboard async function updateScore(userId: string, points: number): Promise { await redis.zincrby('leaderboard:global', points, userId); } async function getTopPlayers(n = 10): Promise<{ userId: string; score: number }[]> { const results = await redis.zrevrangebyscore( 'leaderboard:global', '+inf', '-inf', 'LIMIT', 0, n, 'WITHSCORES' ); const players = []; for (let i = 0; i < results.length; i += 2) { players.push({ userId: results[i], score: parseFloat(results[i + 1]) }); } return players; } async function getUserRank(userId: string): Promise { const rank = await redis.zrevrank('leaderboard:global', userId); return rank !== null ? rank + 1 : null; // 1-indexed }

Persistence Configuration

# redis.conf — production persistence settings # RDB snapshots (every 15 min if at least 1 key changed) save 900 1 save 300 10 save 60 10000 # AOF (Append Only File) for durability appendonly yes appendfsync everysec # Sync every second (balance of safety vs performance) # Memory limits maxmemory 2gb maxmemory-policy allkeys-lru # Evict least recently used keys when memory full # Bind to specific interface bind 127.0.0.1 requirepass your-strong-redis-password

Conclusion

Redis is a force multiplier for web applications. Caching reduces database load by 10-100x. Session storage in Redis is faster and more scalable than database sessions. Pub/Sub enables real-time features without polling. Rate limiting with sorted sets is exact and distributed. BullMQ turns Redis into a reliable job queue. Master these patterns and Redis will become an indispensable part of your infrastructure toolkit.

#Redis#Caching#Backend#Performance#Node.js#Queue
Redis in Production: Caching, Sessions, Pub/Sub & Rate Limiting | Pulse