DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Redis Beyond Caching: Pub/Sub, Sorted Sets, and Rate Limiting

Redis Beyond Caching: Pub/Sub, Sorted Sets, and Rate Limiting

Most developers use Redis for caching. That's 20% of what it can do. Here are the patterns that make Redis indispensable.

Pub/Sub for Real-Time

import { createClient } from 'redis';

const publisher = createClient();
const subscriber = createClient();

await publisher.connect();
await subscriber.connect();

// Subscribe to a channel
await subscriber.subscribe('notifications', (message) => {
  const payload = JSON.parse(message);
  sendToWebSocket(payload.userId, payload);
});

// Publish from anywhere in your app
await publisher.publish('notifications', JSON.stringify({
  userId: '123',
  type: 'order_shipped',
  orderId: '456',
}));
Enter fullscreen mode Exit fullscreen mode

Sorted Sets: Leaderboards and Rankings

// Add users to leaderboard with their score
await redis.zAdd('leaderboard', [
  { score: 2500, value: 'user:alice' },
  { score: 1800, value: 'user:bob' },
  { score: 3200, value: 'user:carol' },
]);

// Get top 10 with scores
const top10 = await redis.zRangeWithScores('leaderboard', 0, 9, { REV: true });

// Get user's rank (0-indexed)
const rank = await redis.zRevRank('leaderboard', 'user:alice');
console.log(`Alice is ranked #${rank + 1}`);

// Increment score
await redis.zIncrBy('leaderboard', 100, 'user:alice');
Enter fullscreen mode Exit fullscreen mode

Rate Limiting With Sliding Window

async function isRateLimited(key: string, limit: number, windowMs: number): Promise<boolean> {
  const now = Date.now();
  const windowStart = now - windowMs;

  const pipeline = redis.multi();
  pipeline.zRemRangeByScore(key, 0, windowStart); // Remove old entries
  pipeline.zAdd(key, [{ score: now, value: `${now}-${Math.random()}` }]);
  pipeline.zCard(key); // Count entries in window
  pipeline.expire(key, Math.ceil(windowMs / 1000));

  const results = await pipeline.exec();
  const count = results[2] as number;

  return count > limit;
}

// Usage
const limited = await isRateLimited(`rate:${userId}`, 100, 60000);
if (limited) return res.status(429).json({ error: 'Rate limit exceeded' });
Enter fullscreen mode Exit fullscreen mode

Distributed Locks

async function withLock<T>(lockKey: string, ttlMs: number, fn: () => Promise<T>): Promise<T> {
  const lockValue = crypto.randomUUID();

  // Acquire lock (SET NX = only set if not exists)
  const acquired = await redis.set(lockKey, lockValue, {
    NX: true,
    PX: ttlMs,
  });

  if (!acquired) throw new Error('Could not acquire lock');

  try {
    return await fn();
  } finally {
    // Only release if we still own it
    const current = await redis.get(lockKey);
    if (current === lockValue) await redis.del(lockKey);
  }
}

// Prevent double-processing a webhook
await withLock(`webhook:${eventId}`, 30000, () => processWebhook(event));
Enter fullscreen mode Exit fullscreen mode

Session Storage

// Store session with 24h TTL
await redis.setEx(
  `session:${sessionId}`,
  86400,
  JSON.stringify({ userId, role, createdAt: Date.now() })
);

// Read session
const session = JSON.parse(await redis.get(`session:${sessionId}`) ?? 'null');
Enter fullscreen mode Exit fullscreen mode

Redis with pub/sub, sorted sets, distributed locks, and session storage are pre-configured in the AI SaaS Starter Kit.

Top comments (0)