Most developers know Redis as a cache: store a JSON blob, set a TTL, avoid hitting the database on every request. That alone is valuable, but it barely scratches the surface. Redis is an in-memory data structure server that supports strings, lists, sets, sorted sets, hashes, streams, and hyperloglogs. Each data structure enables a class of problems that would otherwise require a dedicated system. One Redis instance can replace your cache, your job queue, your rate limiter, your session store, and your real-time event bus.
Pattern 1: Job Queues with Redis Lists
Redis lists with LPUSH and BRPOP create a reliable FIFO queue. The producer pushes jobs to the left; workers block-pop from the right:
import Redis from 'ioredis';
const redis = new Redis();
// Producer: enqueue a job
async function enqueue(queue: string, job: Record<string, unknown>) {
const payload = JSON.stringify({ id: crypto.randomUUID(), data: job, createdAt: Date.now() });
await redis.lpush(queue, payload);
}
// Worker: process jobs with blocking pop
async function startWorker(queue: string, handler: (job: any) => Promise<void>) {
const conn = new Redis(); // Separate connection for blocking ops
while (true) {
const result = await conn.brpop(queue, 30);
if (!result) continue;
const [, payload] = result;
const job = JSON.parse(payload);
try {
await handler(job);
} catch (err) {
await redis.lpush(`${queue}:failed`, payload); // Dead letter queue
}
}
}
await enqueue('emails', { to: 'user@example.com', template: 'welcome' });
startWorker('emails', async (job) => console.log(`Sending to ${job.data.to}`));
For production job queues with retries, delays, priorities, and dashboards, use BullMQ, which builds on this same Redis foundation.
Pattern 2: Rate Limiting with Sorted Sets
The sliding window rate limiter uses a sorted set where the score is the timestamp:
async function isRateLimited(
redis: Redis, key: string, limit: number, windowMs: number
): Promise<{ allowed: boolean; remaining: number }> {
const now = Date.now();
const windowStart = now - windowMs;
const pipeline = redis.pipeline();
pipeline.zremrangebyscore(key, 0, windowStart);
pipeline.zadd(key, now.toString(), `${now}-${Math.random()}`);
pipeline.zcard(key);
pipeline.pexpire(key, windowMs);
const results = await pipeline.exec();
const count = results![2][1] as number;
if (count > limit) {
await redis.zremrangebyscore(key, now, now);
return { allowed: false, remaining: 0 };
}
return { allowed: true, remaining: limit - count };
}
// 100 requests per minute per IP
const result = await isRateLimited(redis, `rate:${ip}`, 100, 60_000);
Pattern 3: Pub/Sub for Real-Time Events
Redis Pub/Sub lets services broadcast events without knowing who is listening. Fire-and-forget messaging for cache invalidation, notifications, and microservice communication:
// Publisher
await redis.publish('orders', JSON.stringify({
type: 'order.placed', orderId: 'ord_abc123', total: 49.99,
}));
// Subscriber
const sub = new Redis();
sub.subscribe('orders', 'inventory');
sub.on('message', (channel, message) => {
const event = JSON.parse(message);
if (channel === 'orders' && event.type === 'order.placed') {
console.log(`New order: ${event.orderId}`);
}
});
Pub/Sub messages are not persisted. If a subscriber is offline, it misses messages. For guaranteed delivery, use Redis Streams with consumer groups.
Pattern 4: Session Storage
Storing sessions in Redis enables stateless application servers. Any instance can serve any user because session data lives in shared Redis:
import session from 'express-session';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';
const redisClient = createClient({ url: process.env.REDIS_URL });
await redisClient.connect();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000,
sameSite: 'lax',
},
}));
Pattern 5: Leaderboards with Sorted Sets
await redis.zadd('leaderboard', 1500, 'player:alice');
await redis.zadd('leaderboard', 2300, 'player:bob');
await redis.zincrby('leaderboard', 100, 'player:alice');
// Top 10 (highest first)
const top10 = await redis.zrevrange('leaderboard', 0, 9, 'WITHSCORES');
// ['player:bob', '2300', 'player:alice', '1600']
// Player rank (0-indexed)
const rank = await redis.zrevrank('leaderboard', 'player:alice'); // 1
Sorted sets maintain order automatically. Adding, updating, and ranking are all O(log n). A million-player leaderboard responds in microseconds.
When Redis Is Not the Answer
Redis stores everything in memory. Data larger than available RAM needs a different solution. Redis persistence (RDB, AOF) provides durability, but it is not a replacement for a primary database. Use Redis for fast, ephemeral, or auxiliary data patterns layered on top of your persistent storage.
Further reading: Redis Documentation | Redis Data Types | BullMQ Documentation

Leave a Reply