Redis in Production: Caching, Sessions, Pub/Sub & Rate Limiting
Rating
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
Write-Through Cache
// Update cache and database simultaneously
async function updateUser(id: string, data: Partial
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
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
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
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.