Redis Pub/Sub & Streams

Redis offers two built-in messaging models with very different guarantees. Pub/Sub is fire-and-forget broadcasting: publishers send to a channel, and whoever is subscribed right now receives it. Nothing is stored. Streams are a persistent, append-only log: messages are kept, consumers read at their own pace, and consumer groups distribute work across workers with acknowledgements and redelivery.

Both are fast and simple to run if you already operate Redis. Picking the right one comes down to whether a missed message matters. For live notifications it usually doesn't; for jobs and events it usually does.

TL;DR

Quick Example

A stream-based job queue with a consumer group:

And Pub/Sub for live notifications:

Core Concepts

Pub/Sub

Keyspace notifications (notify-keyspace-events) use Pub/Sub to announce key changes and expirations, with the same at-most-once caveat.

Streams

A stream is an append-only log of entries. Each entry has a unique, monotonically increasing ID (<ms-timestamp>-<seq>) and a set of field-value pairs.

Consumer Groups

A consumer group tracks a last-delivered ID and a Pending Entries List (PEL) of messages delivered but not yet acknowledged. Each message goes to exactly one consumer in the group, so adding workers scales processing. Multiple groups on the same stream each get every message, which lets fulfillment, analytics, and email each consume independently.

Delivery is at-least-once. If a worker crashes after reading but before XACK, the message stays pending, and another worker can claim it after an idle timeout. Handlers must therefore be idempotent.

Pub/Sub vs Streams vs Lists

Best Practices

Always Trim Streams

Streams grow until trimmed and live in memory. Use XADD ... MAXLEN ~ N (the approximate ~ form is much cheaper) or MINID for time-based retention. Size retention to what consumers need for recovery.

Handle Pending Messages

Run a periodic XAUTOCLAIM sweep so messages held by dead consumers get reprocessed. Track delivery counts from XPENDING, and move messages that fail repeatedly to a dead-letter stream instead of retrying forever.

Make Consumers Idempotent

At-least-once delivery means duplicates happen. Deduplicate by message ID or business key, or make handlers naturally idempotent (upserts, conditional updates).

Use Pub/Sub Only Where Loss Is Acceptable

Cache invalidation hints, typing indicators, and live counters tolerate a missed message. Payment events, orders, and emails don't; use Streams or a durable broker. A common hybrid persists to a Stream (or the database) and uses Pub/Sub only to nudge listeners.

Common Mistakes

Treating Pub/Sub as a Queue

Use a Stream with a consumer group, or a list with LMOVE, for work that must be processed.

Acknowledging Before Processing

Calling XACK immediately after XREADGROUP, before the work is done, turns at-least-once into at-most-once: a crash mid-processing silently drops the message. Acknowledge only after the side effects are committed.

Unbounded Streams on an Evicting Instance

A stream on a cache instance with allkeys-lru can be evicted entirely under memory pressure, or grow until it forces eviction of other keys. Keep streams on a persistent, non-evicting instance with trimming; see Redis persistence.

FAQ

Can Redis Streams replace Kafka?

For moderate throughput and short retention, often yes. Streams give consumer groups, acknowledgements, and replay with far less operational overhead. Kafka wins for very high throughput, long or unlimited retention on disk, partition-level ordering at scale, and a large ecosystem of connectors and stream processors. Streams are bounded by memory; Kafka is bounded by disk.

Does Pub/Sub guarantee delivery?

No. Delivery is at-most-once to currently connected subscribers. There's no storage, acknowledgement, or retry. If a guarantee matters, use Streams, or pair Pub/Sub notifications with a durable store that listeners can catch up from.

How do I preserve message order?

A single stream is totally ordered by entry ID, and each consumer sees its messages in order. With multiple consumers in a group, messages are processed in parallel, so global processing order isn't guaranteed. For per-entity ordering, route each entity to its own stream (or a stream chosen by hashing its ID).

How does Pub/Sub work with WebSockets?

Each WebSocket server subscribes to the channels its connected users need. Any backend service can PUBLISH an event, and every server holding a relevant connection pushes it to the browser. It's the standard way to scale real-time features horizontally. See WebSockets.

Related Topics

References