Skip to content

Fix: Redis Pub/Sub Not Working — Messages Not Received by Subscribers

FixDevs · (Updated: )

Part of:  JavaScript & TypeScript Errors

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 up

Or 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 that catch developers off guard.

The most fundamental constraint is the dedicated connection requirement. Once a Redis client enters subscribe mode (subscribe() or psubscribe()), it can only use pub/sub commands. You cannot call GET, SET, or any other data command on a subscribed client. If you try, Redis returns an error: ERR only (P|S)SUBSCRIBE / (P|S)UNSUBSCRIBE / PING / QUIT / RESET are allowed in this context. This means you always need at least two separate Redis connections — one for subscribing, one for everything else.

The second major surprise is that Pub/Sub has no persistence. Messages are fire-and-forget. If no subscriber is listening at the moment a message is published, that message is gone. There is no queue, no backlog, no delivery guarantee. The PUBLISH command returns the number of subscribers that received the message — if it returns 0, no one heard it, and the message is lost forever.

Other failure patterns:

  • 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.
  • 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 signaturepsubscribe callbacks 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.

Diagnostic Timeline

Your first guess is “check the channel name,” but the channel names match. Messages are still not received. This timeline walks through the real debugging process.

Minute 0 — Verify with redis-cli directly. Open two terminals connected to the same Redis instance. In terminal 1, subscribe. In terminal 2, publish:

# Terminal 1
redis-cli SUBSCRIBE notifications

# Terminal 2
redis-cli PUBLISH notifications "test message"

Terminal 1 should immediately show the message. If it does not, the problem is at the Redis server level (wrong instance, firewall, AUTH). If it does, the problem is in your application code.

Minute 2 — Check whether the subscriber uses a separate connection. This is the single most common cause of Pub/Sub failure. If your code publishes and subscribes on the same Redis client, the subscribe silently fails or the publish throws an error. Look for code that calls both client.subscribe() and client.set() or client.publish() on the same client object:

// WRONG — same client for both
const client = redis.createClient();
await client.subscribe('channel', handler);
await client.publish('channel', 'msg');  // ERR in subscribed state

Minute 5 — Check the PUBLISH return value. After publishing, log the return value:

const receivedBy = await pubClient.publish('notifications', JSON.stringify(data));
console.log('Subscribers who received this message:', receivedBy);

If receivedBy is 0, no subscriber was listening at the time of publish. This means either the subscriber has not connected yet, the channel name does not match, or the subscriber was on a different Redis instance.

Minute 8 — List active channels. From redis-cli or your publisher client, check which channels have active subscribers:

redis-cli PUBSUB CHANNELS '*'
# Returns a list of all channels with at least one subscriber

redis-cli PUBSUB NUMSUB notifications
# Returns the subscriber count for the 'notifications' channel

If your channel does not appear in the list, the subscriber is not subscribed. Check whether the subscribe call completed before the publish.

Minute 12 — Test reconnection behavior. Kill the subscriber’s Redis connection (e.g., redis-cli CLIENT KILL or restart the Redis server). After the client reconnects, publish a message. If the subscriber does not receive it, the re-subscribe logic is missing. The node-redis v4 client does NOT automatically re-subscribe after reconnection — you must listen for the ready event and subscribe again.

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: ioredis re-subscribes to channels automatically on reconnect. The node-redis v4 client (redis npm package) requires manual re-subscription in the 'ready' event handler. If automatic reconnection is critical, ioredis is 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:9

Mix 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 lost

Fix 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 messages

Fix 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 error

Redis 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.

Subscriber callback throws an error — if the message handler throws an unhandled exception, some client libraries silently stop calling the callback for future messages without disconnecting. Wrap the handler in a try/catch:

await subClient.subscribe('events', (message, channel) => {
  try {
    const data = JSON.parse(message);
    processEvent(data);
  } catch (err) {
    console.error(`Error processing message on ${channel}:`, err);
    // Do not rethrow — keep the subscription alive
  }
});

Subscriber overwhelmed by message volume — if the publisher sends messages faster than the subscriber can process them, the Redis client’s TCP buffer fills up. Eventually, Redis disconnects the slow subscriber (see client-output-buffer-limit pubsub in redis.conf). Increase the buffer limit or offload processing to a worker queue:

# redis.conf — default is 32mb hard limit, 8mb soft limit for 60 seconds
client-output-buffer-limit pubsub 256mb 64mb 120

Python redis-py uses blocking threads — in Python, the redis-py subscriber uses a blocking listen() loop that must run in its own thread. If you call pubsub.subscribe() without starting the listener, messages accumulate in the buffer but are never processed:

import redis
import threading

r = redis.Redis()
pubsub = r.pubsub()
pubsub.subscribe('notifications')

# Must run listen() in a thread — it blocks forever
def listener():
    for message in pubsub.listen():
        if message['type'] == 'message':
            print(message['data'])

thread = threading.Thread(target=listener, daemon=True)
thread.start()

For related issues, see Fix: Redis Connection Refused, Fix: Redis Streams Not Working, Fix: Socket.IO Not Connecting, and Fix: Redis Cluster Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles