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
- Pub/Sub:
PUBLISH/SUBSCRIBE; at-most-once delivery; no storage. If a subscriber is disconnected, the message is gone. - Good for real-time fan-out: chat presence, live dashboards, cache invalidation broadcasts, relaying events to WebSocket servers.
- Streams:
XADDappends entries with IDs; data persists until trimmed. - Consumer groups (
XREADGROUP,XACK) give each message to one consumer in the group, with at-least-once delivery via pending-entry tracking. - Reclaim stuck messages with
XAUTOCLAIM; bound memory withMAXLEN/MINIDtrimming. - For very high volumes, long retention, or replay across many teams, prefer Kafka or NATS JetStream.
Quick Example
A stream-based job queue with a consumer group:
And Pub/Sub for live notifications:
Core Concepts
Pub/Sub
SUBSCRIBE channel/PSUBSCRIBE pattern.*registers interest; the connection then enters subscriber mode.PUBLISH channel messagedelivers to every current subscriber and returns how many received it.- No persistence, no acknowledgements, no replay. Disconnected or slow subscribers miss messages. A client whose output buffer overflows is disconnected.
- In Redis Cluster, classic Pub/Sub broadcasts every message to all nodes. Sharded Pub/Sub (
SSUBSCRIBE/SPUBLISH, Redis 7) routes channels to a single shard, so it scales with the cluster.
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
- Redis — The database overview
- Message Queues — Queueing concepts and guarantees
- Kafka — A disk-based distributed log for larger scale
- Event-Driven Architecture — Designing systems around events
- Idempotency — Handling at-least-once delivery safely
- WebSockets — Pushing Pub/Sub events to browsers