Three independent services must each see every event, and each service runs four replicas that should share its own workload. Compare how Redis Pub/Sub and Redis Streams handle that fan-out shape.
answer
- two axes: broadcast across parties, split within a party
- Pub/Sub = broadcast only, no ownership, no ack
- one group per service = independent cursors
- one consumer name per replica = split + PEL + XACK
- no key affinity within a group; shard by stream key
basics
~20 sPub/Sub broadcasts to every subscriber, so all twelve replicas process every event - it cannot load-balance within a service. Redis Streams give both shapes: create one consumer group per service (each group sees all entries) and run the four replicas as consumers inside that group so entries are split among them.
solid answer
~60 sThe requirement mixes two fan-out shapes: **broadcast across services** and **competing consumers within a service**. **Pub/Sub does only broadcast.** Every client subscribed to the channel gets every message, so your four replicas of a service each receive the same event and would all process it. Making them share work requires inventing a de-duplication or partitioning scheme outside Redis - a lock per event, a hash of the payload to decide who owns it - which is fragile and still gives no redelivery when a replica dies mid-work. **Streams do both, by design.** One stream key holds the entries. Create three consumer groups with `XGROUP CREATE events billing $`, `... shipping $`, `... analytics $`. Each group has its own independent cursor, so all three see every entry - that is the broadcast axis. Within a group, `XREADGROUP GROUP billing replica-2 ...` hands each entry to exactly one consumer, and the entry stays in that group's pending list until `XACK` - that is the competing-consumers axis, with redelivery if a replica dies. So: groups fan out, consumers within a group fan in.
code
text · 11 linesXGROUP CREATE events billing $ MKSTREAM
XGROUP CREATE events shipping $ MKSTREAM
XGROUP CREATE events analytics $ MKSTREAM
# each of billing's four replicas, under its own consumer name
XREADGROUP GROUP billing replica-2 COUNT 50 BLOCK 5000 STREAMS events >
XACK events billing 1700000000123-0
# inspect lag and stuck work
XINFO GROUPS events
XPENDING events billinggo deeper
Know that Pub/Sub sends every message to every subscriber, while a consumer group splits entries among its consumers.
Map the requirement to the mechanism: one group per service for broadcast, one consumer name per replica for splitting, with XACK closing the loop.
Discuss the operational edges - pending entries after a crash, trimming versus the slowest group, and why lock-per-message on Pub/Sub is a losing design.
Separate the two fan-out axes explicitly, and be clear about what Redis does not give (key affinity, consumption-aware retention) and what you would build or buy instead.
## Naming the two axes Almost every messaging requirement is a combination of two independent questions: 1. **How many logically distinct parties must see this event?** (broadcast / fan-out) 2. **Within one party, how is the work spread over its processes?** (competing consumers / load balancing) The scenario asks for three parties on axis 1 and four-way sharing on axis 2. Whether a technology can express that shape is the whole question. ## Pub/Sub covers axis 1 only `SUBSCRIBE events` makes a client a recipient of every message on the channel. `PUBLISH` copies the message to all of them. There is no notion of a group, no ownership of a message, no acknowledgement. With twelve subscribed processes, every one of them receives every event. Teams try to force axis 2 on top of it, and the attempts share the same weaknesses: - **Deterministic partitioning** - each replica subscribes to all events but only handles those where `hash(entity_id) % 4 == my_index`. This works while the replica count is fixed and every replica is healthy, but re-indexing during a deploy or a crash means events are dropped or double-handled, and there is no backlog to recover from. - **A lock per event** - every replica tries `SET lock:<event-id> 1 NX EX 60`, and the winner processes it. Now you pay a round trip per event per replica, you must choose a lock TTL, and a crash after winning the lock but before finishing loses the event entirely - because Pub/Sub kept no copy to retry. Both fail on the same root cause: Pub/Sub has no retained copy and no acknowledgement, so there is nothing to redeliver. ## Streams cover both axes natively A stream is one key. Consumer groups are named readers over it: ``` XGROUP CREATE events billing $ MKSTREAM XGROUP CREATE events shipping $ MKSTREAM XGROUP CREATE events analytics $ MKSTREAM ``` Each group carries its own **last-delivered ID**, entirely independent of the others. An entry appended by `XADD` is eligible for delivery to every group exactly once *per group*. That is broadcast across services, but with a per-service cursor rather than a live socket - a group that is down for an hour catches up afterwards. Inside a group, each replica reads under its own consumer name: ``` XREADGROUP GROUP billing replica-2 COUNT 50 BLOCK 5000 STREAMS events > ``` The `>` token means "entries never delivered to this group". Redis assigns each returned entry to that consumer and records it in the group's pending entries list until the consumer sends `XACK`. Two consequences follow: entries are *split* across the four replicas (load balancing), and an entry whose consumer dies before acknowledging is still pending and can be taken over by another consumer. Adding or removing replicas needs no coordination - a new consumer name simply starts pulling unassigned entries. ## What this costs - **Memory.** The stream retains entries for as long as your trimming policy says, and pending-entry bookkeeping is per group. Three groups mean three cursors and three pending lists over one copy of the data - cheap, but not free. - **Explicit acknowledgement.** Every consumer must `XACK`, and someone must handle entries that stay pending because a consumer died - otherwise the pending list grows unbounded and work silently stalls. - **Trimming discipline.** Because trimming is by length or minimum ID and not by "all groups have consumed it", an over-aggressive `MAXLEN` can drop entries a lagging group has not read yet. You must size retention against your slowest consumer's worst outage. ## Where fan-out gets harder than a partitioned log One thing Streams do *not* give you is key-affinity within a group: entries are handed out to whichever consumer asks next, so two events for the same entity can be processed concurrently by different replicas. A partitioned durable log gives ordering per key by construction, because a key always lands in one partition owned by one consumer. In Redis, if you need that, you build it: separate stream keys per shard of the key space (`events:{shard-3}`), with each consumer owning specific keys. That is a real design cost, and it is often the deciding factor when someone asks whether Redis Streams can replace a log-based bus. ## The answer in one breath Use one stream, one consumer group per service, and one consumer name per replica. Groups give you the broadcast axis with independent catch-up; consumers within a group give you competing consumers with acknowledgement and redelivery. Pub/Sub can express only the broadcast axis, and every workaround for the second axis reintroduces the loss problem Pub/Sub already has.
- What happens to entries a consumer received but never acknowledged because it crashed?They stay in that group's pending entries list, attributed to the dead consumer name, and are not redelivered automatically. Another consumer must take ownership - typically by scanning with XPENDING and claiming entries whose idle time exceeds a threshold - after which they are delivered again. If nobody does this, the pending list grows and that work is never completed.
- Can two events for the same customer be processed concurrently by different replicas in one group?Yes. A consumer group distributes entries to whichever consumer asks next; there is no key affinity. If per-entity ordering matters, you must partition yourself - for example by writing to several stream keys chosen by a hash of the entity id and assigning each key to one consumer - or make the handlers commutative and idempotent.
- Why not just have each of the twelve processes subscribe to a Pub/Sub channel and use a Redis lock to pick a winner?It costs an extra round trip per event per process, requires choosing a lock TTL, and - decisively - loses the event if the winner crashes after acquiring the lock, because Pub/Sub retained no copy to retry. A consumer group gives you the same exclusivity with retained entries, acknowledgement and claimable redelivery built in.
Pub/Sub is a loudspeaker in three departments at once - everyone in every room hears everything. Streams are three inboxes fed from one mail room, and inside each department the clerks take letters off their own pile one at a time.
saying these in an interview costs you the question
- Thinking multiple Pub/Sub subscribers on one channel share the messages between them
- Using one consumer group per replica instead of per service, so each replica sees everything
- Assuming unacknowledged entries are redelivered automatically without a claim step
- Expecting per-key ordering inside a consumer group
- Setting MAXLEN aggressively without accounting for the slowest group's outage window