Redis Pub/Sub with WebSocket
Redis WebSocket integration through Pub/Sub enables horizontal scaling of WebSocket servers by allowing multiple server instances to broadcast messages across all connected clients. When you run a single WebSocket server, broadcasting to clients is straightforward—you iterate through connected sockets and send messages. But when you scale to multiple server instances behind a load balancer, clients connected to different servers cannot receive messages from each other without a shared message bus. Redis Pub/Sub solves this by acting as a central coordination layer.
Why You Need Pub/Sub for Multi-Instance WebSocket Servers
A typical WebSocket server maintains connections in memory. When you scale horizontally by adding more server instances, each instance only knows about its own connections. If User A connects to Server 1 and User B connects to Server 2, sending a message from User A to User B requires Server 1 to communicate with Server 2.
Without a message broker, cross-instance communication is impossible. Redis Pub/Sub provides a publish-subscribe pattern where servers publish messages to channels and subscribe to receive messages from those channels. This creates a broadcast mechanism across all server instances, enabling true WebSocket scalability.
The pattern works as follows:
- Each WebSocket server instance subscribes to one or more Redis channels
- When a server receives a message from a client, it publishes that message to Redis
- All subscribed servers (including the originating server) receive the message
- Each server broadcasts the message to its locally connected clients
This architecture allows you to scale WebSocket servers horizontally while maintaining real-time communication across all clients.
Redis Pub/Sub Basics
Redis provides two Pub/Sub mechanisms: channel-based and pattern-based subscriptions.
Channel-based subscription allows you to subscribe to specific channel names:
const Redis = require('ioredis');
const subscriber = new Redis();
subscriber.subscribe('chat:room:1', (err, count) => {
if (err) {
console.error('Subscribe error:', err);
} else {
console.log(`Subscribed to ${count} channel(s)`);
}
});
subscriber.on('message', (channel, message) => {
console.log(`Message from ${channel}:`, message);
});
Pattern-based subscription uses wildcards to subscribe to multiple channels:
subscriber.psubscribe('chat:room:*', (err, count) => {
console.log(`Subscribed to ${count} pattern(s)`);
});
subscriber.on('pmessage', (pattern, channel, message) => {
console.log(`Pattern ${pattern} matched ${channel}:`, message);
});
Publishing messages requires a separate Redis client connection:
const publisher = new Redis();
publisher.publish('chat:room:1', JSON.stringify({
user: 'alice',
text: 'Hello everyone!'
}));
Redis Pub/Sub is fire-and-forget with no persistence. If no subscribers exist when a message is published, the message is lost. This makes it ideal for real-time applications where historical messages are not required or are stored separately.
Node.js WebSocket Server with Redis Pub/Sub
Here’s a complete implementation of a scalable Node.js WebSocket server using Redis pubsub WebSocket pattern:
const WebSocket = require('ws');
const Redis = require('ioredis');
const http = require('http');
// Create Redis clients (subscriber and publisher must be separate)
const subscriber = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379,
retryStrategy: (times) => Math.min(times * 50, 2000)
});
const publisher = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379,
retryStrategy: (times) => Math.min(times * 50, 2000)
});
// Create HTTP server and WebSocket server
const server = http.createServer();
const wss = new WebSocket.Server({ server });
// Track rooms and connected clients
const rooms = new Map(); // roomId -> Set of WebSocket connections
// Subscribe to Redis channels
subscriber.subscribe('broadcast', 'room:*');
// Handle Redis messages
subscriber.on('message', (channel, message) => {
console.log(`Received from Redis [${channel}]:`, message);
try {
const data = JSON.parse(message);
if (channel === 'broadcast') {
// Broadcast to all connected clients
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
} else if (channel.startsWith('room:')) {
// Broadcast to specific room
const roomId = channel.substring(5);
const roomClients = rooms.get(roomId);
if (roomClients) {
roomClients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
}
} catch (err) {
console.error('Error processing Redis message:', err);
}
});
// Handle WebSocket connections
wss.on('connection', (ws) => {
console.log('Client connected');
ws.isAlive = true;
ws.rooms = new Set();
ws.on('pong', () => {
ws.isAlive = true;
});
ws.on('message', (data) => {
try {
const message = JSON.parse(data);
switch (message.type) {
case 'join':
// Join a room
const roomId = message.roomId;
ws.rooms.add(roomId);
if (!rooms.has(roomId)) {
rooms.set(roomId, new Set());
subscriber.subscribe(`room:${roomId}`);
}
rooms.get(roomId).add(ws);
ws.send(JSON.stringify({
type: 'joined',
roomId: roomId
}));
break;
case 'leave':
// Leave a room
const leaveRoomId = message.roomId;
if (ws.rooms.has(leaveRoomId)) {
ws.rooms.delete(leaveRoomId);
const roomClients = rooms.get(leaveRoomId);
if (roomClients) {
roomClients.delete(ws);
if (roomClients.size === 0) {
rooms.delete(leaveRoomId);
subscriber.unsubscribe(`room:${leaveRoomId}`);
}
}
}
break;
case 'message':
// Publish message to Redis
const channel = message.roomId ? `room:${message.roomId}` : 'broadcast';
publisher.publish(channel, JSON.stringify({
type: 'message',
user: message.user,
text: message.text,
timestamp: Date.now()
}));
break;
}
} catch (err) {
console.error('Error processing message:', err);
}
});
ws.on('close', () => {
// Clean up room memberships
ws.rooms.forEach(roomId => {
const roomClients = rooms.get(roomId);
if (roomClients) {
roomClients.delete(ws);
if (roomClients.size === 0) {
rooms.delete(roomId);
subscriber.unsubscribe(`room:${roomId}`);
}
}
});
console.log('Client disconnected');
});
});
// Heartbeat to detect broken connections
const interval = setInterval(() => {
wss.clients.forEach(ws => {
if (ws.isAlive === false) {
return ws.terminate();
}
ws.isAlive = false;
ws.ping();
});
}, 30000);
wss.on('close', () => {
clearInterval(interval);
subscriber.quit();
publisher.quit();
});
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`WebSocket server running on port ${PORT}`);
});
This implementation demonstrates:
- Separate Redis clients for publishing and subscribing
- Room-based messaging with dynamic channel subscriptions
- Cleanup of room subscriptions when rooms become empty
- Connection health monitoring with ping/pong
- Graceful shutdown handling
Scaling Pattern for Redis WebSocket Architecture
When deploying multiple WebSocket server instances with Redis Pub/Sub, follow this architecture:
Load Balancer (nginx/HAProxy)
|
[WebSocket Servers]
/ | \
WS-1 WS-2 WS-3
\ | /
[Redis Pub/Sub]
Each server instance:
- Maintains its own WebSocket connections
- Subscribes to relevant Redis channels on startup
- Publishes client messages to Redis
- Broadcasts received Redis messages to local clients
Key considerations:
Sticky sessions: Use IP hash or cookie-based session affinity in your load balancer to ensure clients reconnect to the same server instance when possible. This reduces reconnection overhead.
Connection state: Store user session data and room memberships in Redis using hash or set data structures, separate from Pub/Sub. This allows any server to reconstruct state if a client reconnects to a different instance.
Channel design: Use hierarchical channel names like app:feature:room:id for logical organization. Pattern subscriptions can match multiple channels efficiently.
Message deduplication: Since the publishing server also receives its own published messages, you may need to track message IDs to avoid double-processing.
Monitoring: Track Redis connection health, pub/sub channel counts, and message throughput. Redis provides PUBSUB CHANNELS and PUBSUB NUMSUB commands for introspection.
MQTT over WebSocket with Mosquitto
MQTT over WebSocket provides a standardized protocol alternative to custom Redis implementations. Mosquitto is a popular open-source MQTT broker that supports WebSocket connections.
Install and configure Mosquitto to enable WebSocket:
protocol mqtt
listener 9001
protocol websockets
Start Mosquitto:
mosquitto -c mosquitto.conf
Client implementation using MQTT.js:
const mqtt = require('mqtt');
// Connect via WebSocket
const client = mqtt.connect('ws://localhost:9001', {
clientId: 'client_' + Math.random().toString(16).substr(2, 8),
clean: true,
reconnectPeriod: 1000
});
client.on('connect', () => {
console.log('Connected to MQTT broker');
// Subscribe to topics
client.subscribe('chat/room/1', (err) => {
if (!err) {
console.log('Subscribed to chat/room/1');
}
});
});
client.on('message', (topic, message) => {
console.log(`Message from ${topic}:`, message.toString());
});
// Publish messages
client.publish('chat/room/1', JSON.stringify({
user: 'alice',
text: 'Hello via MQTT!'
}));
MQTT WebSocket advantages:
- Quality of Service (QoS) levels for guaranteed delivery
- Last Will and Testament for disconnect notifications
- Retained messages for new subscribers
- Wide ecosystem support and client libraries
NATS WebSocket Integration
NATS is a high-performance messaging system with WebSocket support. It offers better performance than Redis Pub/Sub for high-throughput scenarios.
NATS server configuration:
# nats-server.conf
port: 4222
websocket {
port: 8080
no_tls: true
}
Start NATS server:
nats-server -c nats-server.conf
Client implementation:
const { connect } = require('nats.ws');
(async () => {
// Connect via WebSocket
const nc = await connect({
servers: ['ws://localhost:8080']
});
console.log('Connected to NATS');
// Subscribe to subjects
const sub = nc.subscribe('chat.room.1');
(async () => {
for await (const msg of sub) {
console.log(`Message from ${msg.subject}:`,
new TextDecoder().decode(msg.data));
}
})();
// Publish messages
nc.publish('chat.room.1',
new TextEncoder().encode(JSON.stringify({
user: 'alice',
text: 'Hello via NATS!'
}))
);
})();
NATS WebSocket benefits:
- Lower latency than Redis for messaging
- Request-reply patterns built-in
- Subject-based addressing with wildcards
- Clustering and supercluster support
RabbitMQ WebSocket with STOMP
RabbitMQ WebSocket support uses the STOMP protocol over WebSocket. Enable the STOMP plugin:
rabbitmq-plugins enable rabbitmq_web_stomp
This exposes WebSocket on port 15674 by default. Client implementation:
const Stomp = require('stompjs');
// Connect via WebSocket
const client = Stomp.client('ws://localhost:15674/ws');
client.connect('guest', 'guest', () => {
console.log('Connected to RabbitMQ');
// Subscribe to queue
client.subscribe('/topic/chat.room.1', (message) => {
console.log('Received:', message.body);
});
// Send message
client.send('/topic/chat.room.1', {}, JSON.stringify({
user: 'alice',
text: 'Hello via RabbitMQ!'
}));
});
RabbitMQ WebSocket advantages:
- Full AMQP feature set (routing, exchanges, durability)
- Message persistence and acknowledgments
- Complex routing scenarios
- Management UI for monitoring
Comparison Table
| Feature | Redis Pub/Sub | MQTT (Mosquitto) | NATS | RabbitMQ STOMP |
|---|---|---|---|---|
| Protocol | Custom | MQTT 3.1.1/5.0 | NATS | STOMP over WS |
| Message Persistence | No | Retained only | No | Yes (durable queues) |
| Guaranteed Delivery | No | QoS 1/2 | No | Yes (with acks) |
| Performance | High | Medium | Very High | Medium |
| Latency | Low | Medium | Very Low | Medium |
| Complexity | Low | Medium | Low | High |
| Memory Usage | Low | Medium | Low | High |
| Scaling | Horizontal | Clustered | Clustered | Clustered |
| Best For | Simple pub/sub, caching | IoT, mobile apps | Microservices | Enterprise messaging |
| Message Size Limit | 512 MB | 256 MB default | 1 MB default | No hard limit |
When to use each:
-
Redis Pub/Sub: You already use Redis for caching and need simple real-time broadcasting without persistence requirements. Best for chat apps, live updates, notifications.
-
MQTT over WebSocket: IoT applications, mobile apps with unreliable connections, need QoS guarantees, or building on standardized protocols.
-
NATS WebSocket: Microservices architecture requiring high throughput and low latency. Cloud-native applications with dynamic scaling.
-
RabbitMQ WebSocket: Enterprise applications needing message persistence, complex routing, and guaranteed delivery. Legacy system integration.
Frequently Asked Questions
Can I use Redis Pub/Sub for reliable message delivery?
No. Redis Pub/Sub is fire-and-forget with no delivery guarantees. Messages published when no subscribers exist are lost, and disconnected clients miss messages. For reliability, use Redis Streams (which provide consumer groups and acknowledgments) or switch to MQTT with QoS levels or RabbitMQ with persistent queues. Redis Pub/Sub works best for real-time updates where occasional message loss is acceptable.
How many WebSocket connections can a single Redis instance handle?
Redis itself doesn’t maintain WebSocket connections—your application servers do. Redis only handles pub/sub messages between servers. A single Redis instance can handle hundreds of thousands of pub/sub messages per second. The bottleneck is typically your WebSocket server’s connection capacity (10K-100K connections per server depending on resources) rather than Redis. Use Redis Cluster if you need to scale beyond a single Redis instance’s message throughput.
Should I use one Redis channel per WebSocket room or multiplex?
Use separate channels per room for better scalability. Subscribing to room:123 allows servers to only process messages for rooms with active clients. A single multiplexed channel forces all servers to process all messages even for rooms they don’t serve. The exception is global broadcasts (system announcements, notifications) where a single channel makes sense. Pattern subscriptions like room:* provide flexibility while maintaining separation.
What happens if the Redis connection fails?
Your WebSocket servers will continue functioning locally but lose cross-instance communication. Clients connected to the same server can still message each other, but messages won’t reach clients on other servers. Implement Redis reconnection logic with exponential backoff (shown in the example code’s retryStrategy). Monitor Redis connection health and consider queuing messages during disconnection for replay after reconnection. For critical applications, use Redis Sentinel for automatic failover or Redis Cluster for high availability.