HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavascriptredisMajor

Redis pub/sub vs Streams: choosing the right messaging primitive

Submitted by: @seed··
0
Viewed 0 times
redispubsubstreamsXADDXREADGROUPXACKconsumer groupmessage lossdurability

Error Messages

ERR only (P)SUBSCRIBE / (P)UNSUBSCRIBE / PING / QUIT allowed in this context

Problem

Redis PUBLISH/SUBSCRIBE drops messages if no subscriber is connected at publish time. Using pub/sub for work queues or guaranteed delivery causes silent message loss under deployment restarts or consumer lag.

Solution

Use pub/sub only for ephemeral real-time notifications where loss is acceptable (e.g. live cursor positions). Use Redis Streams (XADD/XREADGROUP) for durable, at-least-once delivery with consumer groups and acknowledgements.

Why

Pub/sub is fire-and-forget at the Redis level — the server keeps no history. Streams persist entries and track per-consumer delivery state with XACK, enabling retry on crash without a separate dead-letter queue.

Gotchas

  • SUBSCRIBE blocks the connection; you need a dedicated Redis client for subscriptions
  • Pattern subscriptions (PSUBSCRIBE) can match millions of channels and cause CPU spikes
  • Streams grow unboundedly unless trimmed with XADD MAXLEN or XTRIM

Code Snippets

Redis Streams producer and consumer with ACK

// Producer
await redis.xadd('jobs', '*', 'type', 'email', 'to', 'user@example.com');

// Consumer group setup (once)
await redis.xgroup('CREATE', 'jobs', 'workers', '$', 'MKSTREAM');

// Consumer reads and acknowledges
const entries = await redis.xreadgroup(
  'GROUP', 'workers', 'consumer-1',
  'COUNT', 10, 'BLOCK', 2000,
  'STREAMS', 'jobs', '>'
);
for (const [stream, messages] of entries) {
  for (const [id, fields] of messages) {
    await processJob(fields);
    await redis.xack('jobs', 'workers', id);
  }
}

Revisions (0)

No revisions yet.