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.