Fix: Redis Pub/Sub Not Working — Messages Not Received by Subscribers
Quick Answer
How to fix Redis Pub/Sub issues — subscriber not receiving messages, channel name mismatches, connection handling, pattern subscriptions, and scaling with multiple processes.
The Problem
A Redis Pub/Sub setup publishes messages but subscribers never receive them:
// Publisher
await redisClient.publish('notifications', JSON.stringify({ userId: 42, message: 'Hello' }));
// Returns 0 — zero subscribers received the message
// Subscriber
subClient.subscribe('notifications', (message) => {
console.log('Received:', message);
// This callback never fires
});Or subscribers receive some messages but miss others intermittently:
Published: 1000 messages
Received: 847 messages (153 missed)Or after reconnecting, the subscriber stops receiving messages:
Redis connection lost... reconnecting
Redis reconnected!
// But now messages aren't received even though the connection is upOr pattern subscriptions return unexpected channel names:
subClient.psubscribe('user:*', (message, channel) => {
console.log(channel); // Expected 'user:notifications', got undefined
});Why This Happens
Redis Pub/Sub has several non-obvious behaviors:
- Separate connection required for subscribed clients — once a Redis client enters subscribe mode (
subscribe()orpsubscribe()), it can ONLY use pub/sub commands. You can’t use the same client for both subscribing and publishing (or any other Redis commands). - Channel name mismatch — publisher and subscriber must use the exact same channel string.
'notifications'and'Notifications'are different channels. Trailing spaces or encoding differences cause silent mismatches. - Messages are not persisted — Pub/Sub is fire-and-forget. If no subscriber is listening when a message is published, the message is lost. There’s no queue, no backlog, and no delivery guarantee.
- Subscriptions don’t survive reconnections — when a subscriber connection drops and reconnects, the subscriptions are reset. The client is connected to Redis but no longer subscribed to any channel.
- Pattern subscription callback signature —
psubscribecallbacks receive(message, channel, pattern)in some libraries and(pattern, channel, message)in others. Argument order varies by client library and version. - Redis cluster mode — in a Redis cluster, Pub/Sub messages only reach subscribers connected to the same node that received the publish. Multi-node clusters require special handling.
Fix 1: Use Separate Connections for Pub and Sub
A client in subscribe mode can’t run other commands. Always use two separate client instances:
// WRONG — using one client for both
const client = redis.createClient();
client.subscribe('channel');
client.set('key', 'value'); // ERR Command not allowed in subscribed state
// CORRECT — separate clients
const { createClient } = require('redis');
// Publisher client — used for publish() and all other Redis commands
const pubClient = createClient({ url: process.env.REDIS_URL });
await pubClient.connect();
// Subscriber client — dedicated to subscribe/psubscribe only
const subClient = pubClient.duplicate(); // Copies the connection config
await subClient.connect();
// Subscribe
await subClient.subscribe('notifications', (message, channel) => {
console.log(`Received on ${channel}:`, message);
});
// Publish
await pubClient.publish('notifications', JSON.stringify({ event: 'test' }));createClient().duplicate() creates a new client with the same connection options (URL, password, TLS) but a separate connection. This is the recommended pattern.
Fix 2: Handle Reconnection and Re-subscribe
Redis subscriptions are not automatically restored after reconnection. Listen to reconnect events and re-subscribe:
const { createClient } = require('redis');
async function createSubscriber(channels, messageHandler) {
const client = createClient({ url: process.env.REDIS_URL });
client.on('error', (err) => {
console.error('Redis subscriber error:', err);
});
// Re-subscribe after reconnection
client.on('ready', async () => {
console.log('Redis subscriber connected — subscribing to channels');
try {
await client.subscribe(channels, messageHandler);
} catch (err) {
console.error('Failed to subscribe:', err);
}
});
await client.connect();
return client;
}
// Usage
const subClient = await createSubscriber(
['notifications', 'alerts', 'system'],
(message, channel) => {
console.log(`[${channel}] ${message}`);
handleMessage(channel, JSON.parse(message));
}
);ioredis (alternative Redis client) — built-in reconnect and re-subscribe:
const Redis = require('ioredis');
const subClient = new Redis({
host: 'localhost',
port: 6379,
retryStrategy: (times) => Math.min(times * 100, 3000), // Exponential backoff
reconnectOnError: (err) => {
// Reconnect on specific errors
const targetErrors = ['READONLY', 'ECONNRESET'];
return targetErrors.some(e => err.message.includes(e));
},
enableAutoPipelining: false, // Not compatible with pub/sub mode
});
// ioredis automatically re-subscribes after reconnection
subClient.subscribe('notifications', 'alerts');
subClient.on('message', (channel, message) => {
console.log(`[${channel}]:`, message);
});
subClient.on('error', (err) => {
console.error('Redis error:', err.message);
});Pro Tip:
ioredisre-subscribes to channels automatically on reconnect. Thenode-redisv4 client (redisnpm package) requires manual re-subscription in the'ready'event handler. If automatic reconnection is critical,ioredisis the more resilient choice.
Fix 3: Fix Pattern Subscriptions
psubscribe uses Redis glob patterns and has a different callback signature than subscribe:
// node-redis v4 — psubscribe callback receives (message, channel) for matching channels
await subClient.pSubscribe('user:*', (message, channel) => {
console.log('Channel:', channel); // e.g., 'user:notifications', 'user:alerts'
console.log('Message:', message);
});
// ioredis — pmessage event with (pattern, channel, message)
subClient.psubscribe('user:*');
subClient.on('pmessage', (pattern, channel, message) => {
console.log('Pattern:', pattern); // 'user:*'
console.log('Channel:', channel); // 'user:notifications'
console.log('Message:', message);
});Redis glob pattern syntax:
* — matches any sequence of characters
? — matches exactly one character
[abc] — matches character a, b, or c
[a-z] — matches any character from a to z
Examples:
'user:*' → matches user:123, user:notifications, user:any-string
'order:??:status' → matches order:42:status (exactly 2 chars between colons)
'event:[0-9]' → matches event:0 through event:9Mix subscribe and psubscribe:
// Can subscribe to specific channels AND patterns simultaneously
await subClient.subscribe('system', (message) => {
handleSystemMessage(message);
});
await subClient.pSubscribe('user:*', (message, channel) => {
const userId = channel.split(':')[1];
handleUserMessage(userId, message);
});Fix 4: Serialize and Deserialize Messages Correctly
Redis Pub/Sub transmits strings only. Objects must be serialized to JSON:
// WRONG — publishing an object directly
await pubClient.publish('notifications', { userId: 42, type: 'alert' });
// Subscriber receives '[object Object]' — not the actual object
// CORRECT — serialize to JSON
await pubClient.publish('notifications', JSON.stringify({
userId: 42,
type: 'alert',
timestamp: Date.now(),
}));
// CORRECT — deserialize in subscriber
await subClient.subscribe('notifications', (message, channel) => {
const data = JSON.parse(message);
console.log('UserId:', data.userId);
console.log('Type:', data.type);
});Handle parse errors gracefully:
await subClient.subscribe('events', (rawMessage, channel) => {
let data;
try {
data = JSON.parse(rawMessage);
} catch {
console.error(`Invalid JSON on channel ${channel}:`, rawMessage);
return;
}
processEvent(data);
});Fix 5: Verify Channel Names Match Exactly
The publisher and subscriber must use identical channel strings. Debug by logging the exact channel name being used:
// Use constants to prevent typos
const CHANNELS = Object.freeze({
NOTIFICATIONS: 'notifications:v1',
USER_EVENTS: 'user:events:v1',
SYSTEM_ALERTS: 'system:alerts:v1',
});
// Publisher
await pubClient.publish(CHANNELS.NOTIFICATIONS, JSON.stringify(data));
// Subscriber
await subClient.subscribe(CHANNELS.NOTIFICATIONS, handleNotification);Diagnose from the Redis CLI:
# Terminal 1 — subscribe to a channel
redis-cli SUBSCRIBE notifications
# Terminal 2 — publish a message
redis-cli PUBLISH notifications '{"test": true}'
# Terminal 1 should immediately show:
# 1) "message"
# 2) "notifications"
# 3) "{\"test\": true}"
# If it doesn't, check:
# 1. Are both CLIs connected to the same Redis instance?
# 2. Is the channel name identical?
redis-cli -h <host> -p <port> -a <password> PUBSUB CHANNELS '*'
# Lists all active channels (those with at least one subscriber)Check subscriber count before publishing:
// PUBSUB NUMSUB returns subscriber count per channel
const counts = await pubClient.pubSubNumSub(['notifications', 'alerts']);
console.log('notifications subscribers:', counts['notifications']);
console.log('alerts subscribers:', counts['alerts']);
// If count is 0, no one is listening — messages will be lostFix 6: Scale Pub/Sub Across Multiple Processes
Standard Redis Pub/Sub works within a single Redis node. For multi-process or multi-server setups:
Multiple Node.js processes — each needs its own subscriber:
// In each process — create a dedicated subscriber
// Messages published to Redis are received by ALL subscriber processes
const subClient = pubClient.duplicate();
await subClient.connect();
await subClient.subscribe('notifications', handleNotification);
// Publisher sends once — all subscribers across all processes receive it
await pubClient.publish('notifications', JSON.stringify(data));Socket.IO with multiple instances — use Redis Pub/Sub as the adapter:
const { createAdapter } = require('@socket.io/redis-adapter');
const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
// Socket.IO uses Redis Pub/Sub internally to sync across instances
io.adapter(createAdapter(pubClient, subClient));Redis Cluster — use cluster-compatible pub/sub:
// In Redis Cluster, PUBLISH goes to one shard — subscribers on other shards miss it
// Solution 1: Use Redis Cluster with node-redis v4 (handles cluster pub/sub)
const { createCluster } = require('redis');
const cluster = createCluster({
rootNodes: [
{ url: 'redis://node1:6379' },
{ url: 'redis://node2:6379' },
{ url: 'redis://node3:6379' },
],
});
// Solution 2: Use Keydb or Redis with a single shard for pub/sub
// Solution 3: Use Redis Streams instead of Pub/Sub for persistent, ordered messagesFix 7: Use Redis Streams for Reliability
If message loss is unacceptable, Redis Streams are better than Pub/Sub — they persist messages and support consumer groups:
// Publisher — add to stream (messages persist)
await pubClient.xAdd('notifications', '*', {
userId: '42',
type: 'alert',
message: 'Hello',
timestamp: Date.now().toString(),
});
// Consumer — read from stream with consumer group
await pubClient.xGroupCreate('notifications', 'email-workers', '$', { MKSTREAM: true });
// Worker — read and process messages
async function processNotifications() {
while (true) {
const results = await subClient.xReadGroup(
'email-workers',
'worker-1', // Consumer name (unique per process)
[{ key: 'notifications', id: '>' }], // '>' = undelivered messages
{ COUNT: 10, BLOCK: 5000 }, // Batch of 10, wait 5s if no messages
);
for (const { messages } of results ?? []) {
for (const { id, message } of messages) {
await handleNotification(message);
await subClient.xAck('notifications', 'email-workers', id); // Acknowledge
}
}
}
}Redis Streams survive subscriber disconnections — messages wait in the stream until acknowledged.
Still Not Working?
Check the Redis AUTH — if the Redis server requires a password, both publisher and subscriber connections must authenticate. A subscriber that silently fails authentication connects but never receives messages:
redis-cli -h localhost -p 6379 -a your_password PING
# Must return PONG — if AUTH fails, you get NOAUTH errorRedis version — some Pub/Sub features require specific Redis versions. PUBSUB SHARDCHANNELS and shard pub/sub require Redis 7.0+.
Network firewall blocking — if publisher and subscriber are on different hosts, verify the Redis port (default 6379) is open between them:
nc -zv redis-host 6379
# Connection to redis-host 6379 port [tcp/redis] succeeded!Check if maxmemory-policy evicts pub/sub data — with aggressive memory policies (allkeys-lru), Redis may evict internal pub/sub structures under memory pressure. Use volatile-lru (only evicts keys with TTL) for Pub/Sub workloads.
For related issues, see Fix: Redis Connection Refused and Fix: Celery Task Not Executing.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: AWS SQS Not Working — Messages Not Received, Duplicate Processing, or DLQ Filling Up
How to fix AWS SQS issues — visibility timeout, message not delivered, duplicate messages, Dead Letter Queue configuration, FIFO queue ordering, and Lambda trigger problems.
Fix: Redis Cluster Not Working — MOVED, CROSSSLOT, or Connection Errors
How to fix Redis Cluster errors — MOVED redirects, CROSSSLOT multi-key operations, cluster-aware client setup, hash tags for key grouping, and failover handling.
Fix: Fastify Not Working — 404, Plugin Encapsulation, and Schema Validation Errors
How to fix Fastify issues — route 404 from plugin encapsulation, reply already sent, FST_ERR_VALIDATION, request.body undefined, @fastify/cors, hooks not running, and TypeScript type inference.
Fix: BullMQ Not Working — Jobs Not Processing, Workers Not Starting, or Redis Connection Failing
How to fix BullMQ issues — queue and worker setup, Redis connection, job scheduling, retry strategies, concurrency, rate limiting, event listeners, and dashboard monitoring.