Why this matters
- Checkout emits events (payment captured, shipment queued) that multiple services must consume — Streams give durability and at-least-once delivery; Pub/Sub does not.
- Confusing the two causes lost messages during deploys or network blips when nobody was subscribed.
- Redis Streams are lighter than Kafka for moderate throughput but still require explicit consumer-group design.
Pub/Sub: live notifications
Publishers send to channels; subscribers receive messages in real time. No persistence, no replay, no acks.
SUBSCRIBE order:updates
# separate terminal:
PUBLISH order:updates "{\"orderId\":\"9001\",\"status\":\"SHIPPED\"}"
PSUBSCRIBE inventory:*
PUBLISH inventory:SKU-8842 "restocked"
VaultCommerce pushes "your order shipped" toast notifications to connected browsers via a WebSocket gateway subscribed to order:{userId} channels. If the gateway restarts, missed messages are acceptable — the user refreshes and reads Postgres.
Pub/Sub characteristics
- Ephemeral — no message history; offline subscribers miss data.
- Fan-out — every subscriber on the channel receives a copy.
- Low latency — ideal for live dashboards and typing indicators.
- No ordering guarantees across channels.
Streams: durable event log
Streams append entries with auto IDs (timestamp-sequence). Consumer groups partition work across workers with explicit acknowledgements.
XADD order-events * orderId 9001 event PAYMENT_CAPTURED amount 129.99
XADD order-events * orderId 9001 event FULFILLMENT_QUEUED
XLEN order-events
XRANGE order-events - + COUNT 10
XGROUP CREATE order-events fulfillment $ MKSTREAM
XREADGROUP GROUP fulfillment worker-1 COUNT 5 STREAMS order-events >
XACK order-events fulfillment 1710000000000-0
XPENDING order-events fulfillment
The > ID reads only new messages for the group. Pending entries list (PEL) tracks in-flight work — replay with XCLAIM when a worker dies mid-processing.
VaultCommerce order pipeline
import redis
r = redis.Redis(decode_responses=True)
STREAM = "order-events"
GROUP = "email-service"
def ensure_group():
try:
r.xgroup_create(STREAM, GROUP, id="0", mkstream=True)
except redis.ResponseError as e:
if "BUSYGROUP" not in str(e):
raise
def process_batch(consumer: str, count: int = 10):
entries = r.xreadgroup(GROUP, consumer, {STREAM: ">"}, count=count, block=2000)
for _stream, messages in entries:
for msg_id, fields in messages:
send_receipt_email(fields)
r.xack(STREAM, GROUP, msg_id)
Email and analytics services share the order-events stream via separate consumer groups — each group tracks its own progress.
Quick recall
Everything you need if you only revisit this box.
- Pub/Sub: ephemeral fan-out for live UI updates — no persistence or replay.
- Streams: append-only logs with IDs, range reads, and consumer groups.
XREADGROUP+XACKdelivers at-least-once processing with a pending list.- VaultCommerce uses Pub/Sub for browser toasts and Streams for order side-effects.
- Separate consumer groups let email and analytics progress independently.
- Choose Streams when a missed message during restart is unacceptable.
Test yourself
Answer these before moving on — recall is what makes it stick.