Redis Pub/Sub vs Streams
Redis offers two messaging primitives that look interchangeable from a distance and behave nothing alike up close. Pub/Sub is a megaphone: shout and whoever is listening hears it; everyone else misses it forever. Streams are a ledger: every message gets a permanent slot until you trim it, and consumer groups coordinate who processed what.
I have debugged production incidents where a team used Pub/Sub for order fulfillment because the API felt familiar. Workers restarted, messages vanished, orders stuck. The fix was not "retry harder" — it was switching to Streams with consumer groups and explicit acknowledgment.
Pub/Sub — fire-and-forget broadcast
# Subscriber (blocking connection)
SUBSCRIBE notifications:room-42
# Publisher
PUBLISH notifications:room-42 "{\"type\":\"message\",\"text\":\"hello\"}"
Characteristics:
- At-most-once — no connected subscriber = message gone
- No history — late joiners see nothing from before
- Fan-out — all subscribers on the channel receive every message
- Pattern subscribe —
PSUBSCRIBE notifications:*for wildcard channels
Perfect for WebSocket relay: when a user sends a chat message, publish to the room channel; each WS server subscribed forwards to local connections. If a server was down for two seconds, it does not need the messages from that window — clients reconnect and fetch history from Postgres anyway.
Streams — durable log with consumer groups
XADD orders * type "created" orderId "ord-991" amount "49.99"
XREADGROUP GROUP fulfillers consumer-1 COUNT 10 BLOCK 5000 STREAMS orders >
XACK orders fulfillers 1680000000000-0
Characteristics:
- Persistent — messages survive until trimmed
- Consumer groups — each message delivered to one consumer in group
- Pending list — track in-flight; reclaim on crash with XCLAIM
- Replay — read from any ID or time range
Perfect for order processing, audit trails, and any workflow where "message lost on restart" is unacceptable.
Side-by-side comparison
| Pub/Sub | Streams | |
|---|---|---|
| Persistence | None | Yes (until trimmed) |
| Delivery | All live subscribers | One per consumer group |
| Replay | No | Yes |
| Acknowledgment | No | XACK |
| Backpressure | Drop (slow client buffers) | Consumer pulls at own pace |
| Latency | Lower | Slightly higher |
Hybrid architecture (common in practice)
Most apps I see use both:
Order created → XADD orders (durable processing)
→ PUBLISH dashboard:metrics (live counter tick)
Cache invalidation → PUBLISH cache:invalidate product:42
Presence update → PUBLISH presence:room-7
Background job → XADD jobs + consumer group
Pub/Sub for signals; Streams for work.
Pub/Sub gotchas
Subscriber connection is dedicated. A connection in SUBSCRIBE mode cannot run other commands except unsubscribe variants. Use a separate connection pool for Pub/Sub listeners vs regular commands.
No backpressure semantics. Slow subscriber buffers grow until disconnect. Do not put heavy processing in the subscribe loop — enqueue locally and process async.
Cluster caveat. Pub/Sub in Redis Cluster only delivers to subscribers on the same node unless using sharded pub/sub (Redis 7+). Plan channel-to-slot mapping or use centralized relay.
Streams gotchas
Memory growth. Unbounded XADD without MAXLEN ~ trimming fills RAM. Set approximate max length or trim on schedule.
Idempotent consumers. XACK after process means redelivery on crash — handlers must tolerate duplicates.
Not a full event bus. No schema registry, no cross-datacenter replication story built in. Know your limits.
Migration path: Pub/Sub to Streams
When you outgrow Pub/Sub:
- Identify messages that must not be lost (jobs, payments, emails).
- Replace
PUBLISHwithXADDon a stream per domain. - Add consumer group workers with XREADGROUP loop.
- Keep Pub/Sub for UI fan-out if needed — dual-write during transition.
- Monitor pending list length (XPENDING) for stuck consumers.
Monitoring both primitives
For Pub/Sub, track subscriber count and message publish rate — sudden drops in subscribers may indicate connection leaks. For Streams, alert on stream length growth, consumer group lag (messages not yet delivered to any consumer), and pending entries list size (messages delivered but not acked). Grafana dashboards combining Redis INFO and XPENDING output catch stuck consumers before backlog spans hours.
Choosing pub/sub vs streams
Use Pub/Sub when: fire-and-forget notifications, subscribers online, message loss OK. Use Streams when: consumer groups, at-least-once, replay needed, audit trail.
Never use Pub/Sub for payment events — disconnected consumer loses messages permanently.
Common production mistakes
Teams get pub sub vs streams wrong in predictable ways:
- Skipping failure-mode rehearsal — run a game day or fault injection exercise before peak traffic, not after the first outage.
- Missing correlation context — every error path should carry request, trace, or tenant identifiers so incidents are debuggable.
- Optimizing for demo, not steady state — load tests, cache warm-up, and cold-start paths matter more than local dev latency.
- Undocumented trade-offs — if you chose speed over strict correctness (or vice versa), write that down for the next engineer.
Redis usage for pub sub vs streams loses data when persistence mode is misunderstood, hot keys saturate single shards, and TTL strategy is applied after memory pressure already triggered evictions.
Debugging and triage workflow
When pub sub vs streams misbehaves in production, work top-down instead of guessing:
- Confirm scope — one tenant, region, or deployment stage? Narrow blast radius before deep diving.
- Check recent changes — deploys, flag flips, config pushes, and schema migrations in the last 24 hours.
- Compare golden signals — latency, error rate, saturation, and traffic for the affected surface vs. baseline.
- Reproduce minimally — smallest input or scenario that triggers the failure; capture traces/logs with correlation IDs.
- Fix forward or rollback — if rollback is faster than root-cause during incident, rollback first, postmortem second.
- Add a guard — alert, integration test, or circuit breaker so the same class of failure is caught earlier next time.
Document the timeline during triage. Future you (and on-call) will need timestamps, not just conclusions.
Resources
- Redis Pub/Sub documentation
- Redis Streams introduction
- XREADGROUP command
- Sharded Pub/Sub (Redis 7)
- Event Processing with Redis Streams
Frequently asked questions
What is the main difference between Redis Pub/Sub and Streams?
Pub/Sub delivers messages only to subscribers connected at publish time — no persistence, no replay, no acknowledgment. Streams append messages to a durable log with IDs, support consumer groups for load-balanced processing, and allow replay from any point. Pub/Sub is a broadcast channel; Streams are an event log.
When should I use Redis Pub/Sub?
Use Pub/Sub for ephemeral notifications where loss is acceptable: live UI updates, cache invalidation broadcasts, presence fan-out, or triggering WebSocket pushes. It is low-latency and simple. If a subscriber is offline, it misses the message — by design.
Can Redis Streams replace Kafka?
For moderate throughput (thousands to low tens of thousands of events per second), short-to-medium retention, and teams already running Redis, Streams can replace Kafka for many internal event pipelines. Kafka wins on very high throughput, long retention, partition ordering at scale, and ecosystem connectors.
Hiring a senior Android / Flutter engineer?
I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.
Get in touch →