Skip to main content
Technology & EngineeringRedis177 lines

Pub Sub

Redis Pub/Sub messaging patterns for real-time event broadcasting

Quick Summary16 lines
You are an expert in Redis Pub/Sub for building real-time messaging and event broadcasting systems.

## Key Points

- **Use separate Redis clients** for subscriptions and regular commands. A subscribed client is blocked from issuing non-subscribe commands.
- **Keep messages small.** Pub/Sub messages pass through the Redis event loop. Large payloads (>1 KB) at high volume degrade server throughput.
- **Use pattern subscriptions sparingly.** Each incoming message is matched against every active pattern, adding CPU overhead proportional to pattern count.
- **Namespace channels** with a consistent convention (e.g., `service:entity:event`) to avoid collisions and simplify pattern subscriptions.
- **Handle reconnection.** When a subscriber disconnects and reconnects, it must resubscribe. ioredis handles this automatically if `autoResubscribe` is enabled (default).
- **Assuming message durability.** Pub/Sub is fire-and-forget. If no subscriber is listening, the message vanishes. For durable messaging, use Redis Streams or an external message broker.
- **Using a single connection for subscribe and commands.** This causes errors or silently dropped commands. Always use a dedicated connection for subscriptions.
- **Blocking the message handler.** If `sub.on("message", ...)` performs slow async work, message processing backs up. Offload heavy processing to a worker queue.
- **Not monitoring `subscription` count.** If pattern subscriptions grow unbounded (e.g., one per user), Redis CPU usage spikes. Check `PUBSUB NUMPAT` periodically.
- **Ignoring the subscriber count return from PUBLISH.** A return of 0 means nobody received the message, which may indicate a deployment or connectivity issue.
skilldb get redis-skills/Pub SubFull skill: 177 lines
Paste into your CLAUDE.md or agent config

Pub/Sub — Redis

You are an expert in Redis Pub/Sub for building real-time messaging and event broadcasting systems.

Core Philosophy

Overview

Redis Pub/Sub provides a fire-and-forget messaging system where publishers send messages to channels and subscribers receive them in real time. Messages are not persisted; if a subscriber is offline when a message is published, that message is lost. This makes Pub/Sub ideal for ephemeral notifications, real-time updates, and inter-service signaling where at-most-once delivery is acceptable.

Core Concepts

Channels

Named channels act as message topics. Publishers send to a channel name; all active subscribers on that channel receive the message. Channel names are arbitrary strings, commonly using colon-separated namespaces (e.g., notifications:user:42).

Pattern Subscriptions

Subscribers can use glob-style patterns (PSUBSCRIBE) to receive messages from all channels matching a pattern, such as notifications:*.

Dedicated Connections

A Redis connection in subscribe mode cannot issue other commands. Applications must use separate Redis client instances for subscribing and for normal operations.

Message Ordering

Messages on a single channel are delivered in the order they were published. There is no ordering guarantee across channels.

Implementation Patterns

Basic publish and subscribe

import Redis from "ioredis";

// Subscriber — must be a dedicated connection
const sub = new Redis();
// Publisher — separate connection
const pub = new Redis();

sub.subscribe("events:order-created", (err, count) => {
  if (err) throw err;
  console.log(`Subscribed to ${count} channel(s)`);
});

sub.on("message", (channel, message) => {
  const event = JSON.parse(message);
  console.log(`[${channel}] Order ${event.orderId} created`);
});

// Publish from another part of the application
await pub.publish(
  "events:order-created",
  JSON.stringify({ orderId: "abc-123", total: 59.99, ts: Date.now() })
);

Pattern subscription

const sub = new Redis();

sub.psubscribe("events:*", (err) => {
  if (err) throw err;
});

sub.on("pmessage", (pattern, channel, message) => {
  // pattern = "events:*"
  // channel = "events:order-created" (the actual channel)
  console.log(`Pattern ${pattern} matched channel ${channel}`);
  handleEvent(channel, JSON.parse(message));
});

Chat room with multiple channels

class ChatService {
  private sub: Redis;
  private pub: Redis;

  constructor() {
    this.sub = new Redis();
    this.pub = new Redis();
  }

  async joinRoom(roomId: string, onMessage: (msg: ChatMessage) => void) {
    const channel = `chat:room:${roomId}`;
    await this.sub.subscribe(channel);

    this.sub.on("message", (ch, raw) => {
      if (ch === channel) {
        onMessage(JSON.parse(raw));
      }
    });
  }

  async sendMessage(roomId: string, userId: string, text: string) {
    const msg: ChatMessage = { userId, text, ts: Date.now() };
    await this.pub.publish(`chat:room:${roomId}`, JSON.stringify(msg));
  }

  async leaveRoom(roomId: string) {
    await this.sub.unsubscribe(`chat:room:${roomId}`);
  }
}

Cache invalidation broadcast

// When any app instance updates a cached entity, broadcast invalidation
async function invalidateCache(entityType: string, entityId: string) {
  // Delete locally
  localCache.delete(`${entityType}:${entityId}`);

  // Notify all other instances
  await pub.publish(
    "cache:invalidate",
    JSON.stringify({ entityType, entityId })
  );
}

// Every instance subscribes on startup
sub.subscribe("cache:invalidate");
sub.on("message", (channel, message) => {
  if (channel === "cache:invalidate") {
    const { entityType, entityId } = JSON.parse(message);
    localCache.delete(`${entityType}:${entityId}`);
  }
});

Extracting subscriber count

// PUBLISH returns the number of subscribers that received the message
const receiverCount = await pub.publish("events:deploy", JSON.stringify({ version: "2.1.0" }));
console.log(`Message delivered to ${receiverCount} subscriber(s)`);

// PUBSUB NUMSUB returns subscriber counts per channel
const counts = await pub.pubsub("NUMSUB", "events:deploy", "events:rollback");
// Returns: ["events:deploy", "3", "events:rollback", "0"]

Best Practices

  • Use separate Redis clients for subscriptions and regular commands. A subscribed client is blocked from issuing non-subscribe commands.
  • Keep messages small. Pub/Sub messages pass through the Redis event loop. Large payloads (>1 KB) at high volume degrade server throughput.
  • Use pattern subscriptions sparingly. Each incoming message is matched against every active pattern, adding CPU overhead proportional to pattern count.
  • Namespace channels with a consistent convention (e.g., service:entity:event) to avoid collisions and simplify pattern subscriptions.
  • Handle reconnection. When a subscriber disconnects and reconnects, it must resubscribe. ioredis handles this automatically if autoResubscribe is enabled (default).

Common Pitfalls

  • Assuming message durability. Pub/Sub is fire-and-forget. If no subscriber is listening, the message vanishes. For durable messaging, use Redis Streams or an external message broker.
  • Using a single connection for subscribe and commands. This causes errors or silently dropped commands. Always use a dedicated connection for subscriptions.
  • Blocking the message handler. If sub.on("message", ...) performs slow async work, message processing backs up. Offload heavy processing to a worker queue.
  • Not monitoring subscription count. If pattern subscriptions grow unbounded (e.g., one per user), Redis CPU usage spikes. Check PUBSUB NUMPAT periodically.
  • Ignoring the subscriber count return from PUBLISH. A return of 0 means nobody received the message, which may indicate a deployment or connectivity issue.

Anti-Patterns

Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.

Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.

Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add redis-skills

Get CLI access →