Skip to content

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

FixDevs ·

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:

  • Separate connection required for subscribed clients — once a Redis client enters subscribe mode (subscribe() or psubscribe()), 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 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.

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.

For related issues, see Fix: Redis Connection Refused and Fix: Celery Task Not Executing.

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