Redis Pub/Sub: Message Broadcasting

Redis Pub/Sub implements a publish-subscribe messaging paradigm where publishers send messages to channels without knowledge of subscribers, and subscribers listen to channels without knowing about...

Key Insights

  • Redis Pub/Sub provides a lightweight, fire-and-forget messaging pattern ideal for real-time notifications, chat systems, and event broadcasting where message persistence isn’t required
  • Subscribers only receive messages published after they connect, making it unsuitable for guaranteed delivery scenarios—use Redis Streams or a message queue for durability
  • Pattern-based subscriptions enable flexible message routing without complex topic hierarchies, but beware of the performance impact when using wildcard patterns at scale

Understanding Redis Pub/Sub Architecture

Redis Pub/Sub implements a publish-subscribe messaging paradigm where publishers send messages to channels without knowledge of subscribers, and subscribers listen to channels without knowing about publishers. This decoupling makes it excellent for broadcasting events across distributed systems.

The model operates entirely in-memory with zero message persistence. When a message is published to a channel, Redis immediately delivers it to all active subscribers. If no subscribers exist, the message disappears. This fire-and-forget approach trades durability for speed—latency typically measures in microseconds.

import redis

# Create Redis clients
publisher = redis.Redis(host='localhost', port=6379, decode_responses=True)
subscriber = redis.Redis(host='localhost', port=6379, decode_responses=True)

# Subscribe to a channel
pubsub = subscriber.pubsub()
pubsub.subscribe('notifications')

# Publish a message
publisher.publish('notifications', 'User logged in')

# Receive the message
for message in pubsub.listen():
    if message['type'] == 'message':
        print(f"Received: {message['data']}")
        break

Implementing Multi-Channel Subscriptions

Applications often need to listen to multiple channels simultaneously. Redis Pub/Sub handles this efficiently through a single connection, reducing overhead compared to maintaining separate connections per channel.

const redis = require('redis');

const subscriber = redis.createClient();

subscriber.on('message', (channel, message) => {
    console.log(`Channel ${channel}: ${message}`);
    
    // Route based on channel
    switch(channel) {
        case 'orders':
            processOrder(JSON.parse(message));
            break;
        case 'inventory':
            updateInventory(JSON.parse(message));
            break;
        case 'alerts':
            sendAlert(message);
            break;
    }
});

// Subscribe to multiple channels
subscriber.subscribe('orders', 'inventory', 'alerts');

// Publisher side
const publisher = redis.createClient();

publisher.publish('orders', JSON.stringify({
    orderId: '12345',
    customerId: 'C789',
    amount: 99.99
}));

publisher.publish('inventory', JSON.stringify({
    productId: 'P456',
    quantity: -1
}));

Pattern-Based Subscriptions

Pattern subscriptions use glob-style matching to subscribe to multiple channels with a single command. This proves valuable when working with hierarchical channel naming schemes.

package main

import (
    "context"
    "fmt"
    "github.com/redis/go-redis/v9"
)

func main() {
    ctx := context.Background()
    rdb := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })

    // Subscribe to all channels matching pattern
    pubsub := rdb.PSubscribe(ctx, "logs:*")
    defer pubsub.Close()

    // Publisher publishes to specific channels
    go func() {
        rdb.Publish(ctx, "logs:error", "Database connection failed")
        rdb.Publish(ctx, "logs:info", "Service started")
        rdb.Publish(ctx, "logs:warning", "High memory usage")
    }()

    // Receive messages from all matching channels
    ch := pubsub.Channel()
    for msg := range ch {
        fmt.Printf("Channel: %s, Message: %s\n", msg.Channel, msg.Payload)
    }
}

Pattern subscriptions support * (any characters) and ? (single character) wildcards. However, each pattern requires Redis to evaluate every published message against it, creating O(N*M) complexity where N is the number of patterns and M is the number of channels. Keep pattern counts low in high-throughput systems.

Building a Real-Time Chat System

A practical implementation demonstrates Pub/Sub’s strengths: a real-time chat room where messages broadcast instantly to all connected users.

import redis
import json
import threading
from datetime import datetime

class ChatRoom:
    def __init__(self, room_id, username):
        self.room_id = room_id
        self.username = username
        self.redis_client = redis.Redis(host='localhost', port=6379, 
                                       decode_responses=True)
        self.pubsub = self.redis_client.pubsub()
        self.channel = f"chat:{room_id}"
        
    def subscribe(self):
        """Listen for messages in the chat room"""
        self.pubsub.subscribe(self.channel)
        print(f"{self.username} joined {self.room_id}")
        
        for message in self.pubsub.listen():
            if message['type'] == 'message':
                data = json.loads(message['data'])
                if data['username'] != self.username:
                    print(f"[{data['timestamp']}] {data['username']}: {data['text']}")
    
    def publish(self, text):
        """Send a message to the chat room"""
        message = {
            'username': self.username,
            'text': text,
            'timestamp': datetime.now().isoformat()
        }
        self.redis_client.publish(self.channel, json.dumps(message))
    
    def leave(self):
        """Unsubscribe from the chat room"""
        self.pubsub.unsubscribe(self.channel)
        print(f"{self.username} left {self.room_id}")

# Usage
chat = ChatRoom('general', 'alice')

# Start listener in background thread
listener = threading.Thread(target=chat.subscribe, daemon=True)
listener.start()

# Send messages
chat.publish("Hello everyone!")
chat.publish("How's it going?")

Handling Connection Failures and Reconnection

Redis Pub/Sub connections require active management. Network interruptions or Redis restarts break subscriptions, and clients must resubscribe upon reconnection.

const redis = require('redis');

class ResilientSubscriber {
    constructor(channels) {
        this.channels = channels;
        this.client = null;
        this.reconnectAttempts = 0;
        this.maxReconnectAttempts = 10;
        this.connect();
    }

    connect() {
        this.client = redis.createClient({
            socket: {
                reconnectStrategy: (retries) => {
                    if (retries > this.maxReconnectAttempts) {
                        console.error('Max reconnection attempts reached');
                        return new Error('Max reconnection attempts exceeded');
                    }
                    // Exponential backoff
                    return Math.min(retries * 100, 3000);
                }
            }
        });

        this.client.on('error', (err) => {
            console.error('Redis Client Error', err);
        });

        this.client.on('ready', () => {
            console.log('Redis client ready');
            this.subscribe();
        });

        this.client.on('reconnecting', () => {
            console.log('Redis client reconnecting');
        });

        this.client.connect();
    }

    subscribe() {
        this.channels.forEach(channel => {
            this.client.subscribe(channel, (message, channel) => {
                this.handleMessage(channel, message);
            });
        });
        console.log(`Subscribed to: ${this.channels.join(', ')}`);
    }

    handleMessage(channel, message) {
        console.log(`[${channel}] ${message}`);
        // Process message
    }
}

const subscriber = new ResilientSubscriber(['events', 'notifications']);

Monitoring and Debugging Pub/Sub

Redis provides commands to inspect active Pub/Sub activity, essential for troubleshooting production issues.

# List all active channels with at least one subscriber
redis-cli PUBSUB CHANNELS

# List channels matching a pattern
redis-cli PUBSUB CHANNELS "logs:*"

# Count subscribers on a specific channel
redis-cli PUBSUB NUMSUB orders inventory

# Count total pattern subscriptions
redis-cli PUBSUB NUMPAT

Implement monitoring in your application:

import redis

def monitor_pubsub_health(redis_client):
    """Check Pub/Sub system health"""
    
    # Get all active channels
    channels = redis_client.pubsub_channels()
    print(f"Active channels: {len(channels)}")
    
    # Get subscriber counts
    if channels:
        subscriber_counts = redis_client.pubsub_numsub(*channels)
        for i in range(0, len(subscriber_counts), 2):
            channel = subscriber_counts[i]
            count = subscriber_counts[i + 1]
            print(f"  {channel}: {count} subscribers")
    
    # Get pattern subscription count
    pattern_count = redis_client.pubsub_numpat()
    print(f"Active pattern subscriptions: {pattern_count}")

r = redis.Redis(host='localhost', port=6379, decode_responses=True)
monitor_pubsub_health(r)

Performance Considerations and Limitations

Redis Pub/Sub excels at low-latency message broadcasting but has clear boundaries. Messages consume network bandwidth proportional to the subscriber count—publishing to a channel with 1000 subscribers sends 1000 copies. This limits scalability for high-frequency updates with many subscribers.

The lack of message queuing means slow subscribers don’t back-pressure publishers. Redis maintains an output buffer per client, and if a subscriber can’t keep up, its buffer grows until hitting the client-output-buffer-limit, causing disconnection.

# Configure client output buffer limits in redis.conf
# Format: client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>

# Pub/Sub clients - disconnect if buffer exceeds 32MB or stays above 8MB for 60s
client-output-buffer-limit pubsub 32mb 8mb 60

For guaranteed delivery, message persistence, or complex routing, consider Redis Streams or dedicated message brokers. Use Pub/Sub when you need simple, fast broadcasting where occasional message loss is acceptable—real-time dashboards, live notifications, or cache invalidation signals across application instances.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.