All posts/
RedisBackendArchitecture

Redis Beyond Caching: Pub/Sub, Streams, and Rate Limiting

Nov 18, 2025·9 min read

Most developers use Redis only as a cache. Explore its full potential — real-time pub/sub messaging, consumer groups with Redis Streams, and bulletproof rate limiting strategies.

Redis as a Message Broker (Pub/Sub)

Redis Pub/Sub allows services to communicate over named channels without knowing about each other. Publishers push messages; any number of subscribers receive them in real time — perfect for notifications, live dashboards, and chat.

javascript
// publisher.js
const redis = require('ioredis');
const pub = new redis();

setInterval(() => {
  pub.publish('metrics', JSON.stringify({ cpu: process.cpuUsage() }));
}, 1000);

// subscriber.js
const sub = new redis();
sub.subscribe('metrics');
sub.on('message', (_channel, message) => {
  const { cpu } = JSON.parse(message);
  console.log('CPU:', cpu);
});

Redis Streams

Streams are Redis's append-only log. Unlike Pub/Sub, messages are persisted so late consumers can replay history. Consumer groups let multiple workers share the load without duplicate processing.

javascript
// Producer — append event to stream
await redis.xadd('orders', '*', 'orderId', '123', 'item', 'laptop');

// Consumer group setup (once)
await redis.xgroup('CREATE', 'orders', 'processors', '$', 'MKSTREAM');

// Worker — read new entries from the group
const entries = await redis.xreadgroup(
  'GROUP', 'processors', 'worker-1',
  'COUNT', 10, 'BLOCK', 2000,
  'STREAMS', 'orders', '>',
);
// Acknowledge after processing
await redis.xack('orders', 'processors', entryId);

Sliding Window Rate Limiting

Use a sorted set to implement a sliding-window rate limiter. Score each request by its timestamp and count entries within the window — atomic, accurate, and no cron cleanup needed.

javascript
async function isAllowed(userId, limit = 100, windowMs = 60_000) {
  const key   = `rl:${userId}`;
  const now   = Date.now();
  const floor = now - windowMs;

  const [, , count] = await redis
    .multi()
    .zremrangebyscore(key, '-inf', floor)   // prune old entries
    .zadd(key, now, `${now}-${Math.random()}`) // record this request
    .zcard(key)                              // count within window
    .expire(key, Math.ceil(windowMs / 1000))
    .exec();

  return count <= limit;
}
Think of Redis not as a cache bolted onto your stack, but as a real-time data infrastructure layer.
PreviousNext.js 15 App Router: Server Components & Streaming ExplainedNextCI/CD Pipelines with GitHub Actions: Zero to Production