Backend / Caching / Redis / 06_pubsub_and_streams.md

Pub/sub and Streams

Updated 6 interview angles 4 min read source
On this page8
  1. Pub/sub is fire-and-forget
  2. Streams are an append-only log
  3. Recovering a dead consumer’s work
  4. Trimming, because a log grows
  5. The comparison
  6. And against a real broker
  7. Related
  8. Interview angle

Pub/sub and Streams

Redis has two messaging primitives with the same shape and opposite guarantees. Choosing pub/sub for something that matters is a recurring production mistake, and the interview question is whether you know why.

Pub/sub is fire-and-forget

python
# Publisher
r.publish("orders", json.dumps(event))

# Subscriber
p = r.pubsub()
p.subscribe("orders")
for msg in p.listen():
    handle(msg["data"])

A message is delivered to whoever is connected at that instant, and then it is gone. No storage, no acknowledgement, no replay, no consumer groups. A subscriber that is restarting, redeploying or briefly disconnected simply misses everything sent in that window, and nothing anywhere records that it happened.

PUBLISH returns the number of subscribers that received it — which is the only feedback you get, and it is zero when nobody is listening.

Legitimate uses: cache invalidation broadcasts, live dashboards, presence, “something changed, go look”. Everything there tolerates a miss because a later message corrects it.

Streams are an append-only log

python
# Producer — the id is assigned and ordered.
r.xadd("orders", {"id": order_id, "total": "42.00"}, maxlen=100_000,
       approximate=True)

# Consumer group, created once.
r.xgroup_create("orders", "billing", id="0", mkstream=True)
python
while True:
    msgs = r.xreadgroup("billing", worker_name, {"orders": ">"},
                        count=10, block=5000)
    for _, entries in msgs:
        for msg_id, fields in entries:
            handle(fields)
            # explicit
            r.xack("orders", "billing", msg_id)

Everything pub/sub lacks is here: messages persist, > delivers only what this group has not seen, and an unacknowledged message stays in the group’s pending list so a crashed worker’s work is recoverable rather than lost.

Recovering a dead consumer’s work

The pending list is the mechanism, and XAUTOCLAIM is how you drain it:

python
# Reassign anything pending for more than 60s to me.
r.xautoclaim("orders", "billing", worker_name,
             min_idle_time=60_000, count=10)

Without this, a worker that dies mid-message leaves that message pending forever — delivered, never acknowledged, never retried. A claim loop is not optional in production, and forgetting it is the Streams equivalent of choosing pub/sub.

A message claimed repeatedly is a poison pill: check its delivery count and route it to a dead-letter stream rather than looping.

Trimming, because a log grows

XADD with maxlen and approximate=True trims as it writes and is far cheaper than exact trimming, since it only removes whole nodes. Without a maxlen or a periodic XTRIM MINID, the stream grows until memory runs out — this is the same failure as unpruned execution data anywhere else.

The comparison

Pub/sub Streams
Persistence none until trimmed
Missed while offline lost delivered on reconnect
Acknowledgement none XACK, pending list
Consumer groups no yes
Replay no by id
Ordering per publisher total, by id

And against a real broker

Streams cover a lot of what people use RabbitMQ or Kafka for, and the honest limits are worth stating rather than defending:

  • No routing. No exchanges, no topic patterns — see RabbitMQ exchanges and routing.
  • Retention is memory. Kafka retains days on disk cheaply; Redis retains in RAM, so long retention is expensive.
  • Durability is Redis’s durability — asynchronous replication, so a failover can lose recent entries.

The defensible position: Streams if you already run Redis and the volume is moderate; a broker when the log is the system of record. See Kafka vs RabbitMQ — choosing a message broker.

Interview angle 6

  • “Pub/sub or Streams?” - pub/sub is fire-and-forget with no storage, no acknowledgement and no replay, so anyone disconnected misses the message permanently. Streams are an append-only log with consumer groups and explicit XACK. Anything that matters uses Streams.
  • “When is pub/sub the right choice?” - broadcasts where a miss is self-correcting: cache invalidation, live dashboards, presence. A later message fixes what an earlier one missed.
  • “How does a Redis Stream consumer group work?” - XREADGROUP with > delivers only unseen messages, each goes to one member, and it stays in the group’s pending list until XACK. That pending list is what makes a crashed worker recoverable.
  • “A worker died mid-message. What happens?” - the message stays pending forever unless something claims it. XAUTOCLAIM with a min_idle_time reassigns it; without a claim loop the work is delivered, never acknowledged, and never retried.
  • “What stops a Stream growing forever?” - XADD with maxlen and approximate=True, which trims whole nodes cheaply as it writes, or a periodic XTRIM MINID. Redis retains in RAM, so unbounded retention is an outage.
  • “Streams or Kafka?” - Streams if you already run Redis and volume is moderate. Kafka when the log is the system of record: disk-cheap long retention, real routing and partition-level ordering guarantees Redis does not offer.