Redis Is Three Messaging Systems Pretending to Be One
January 6, 2026
Redis offers three primitives that all look like messaging from the outside, and they have wildly different delivery semantics. The wrong choice does not surface in development. It surfaces during a deploy or a network blip, when messages quietly stop arriving.
Pub/Sub is at-most-once fan-out. A publisher calls PUBLISH and Redis pushes the message to every currently connected subscriber. There is no persistence. A subscriber that is restarting, deploying, or briefly disconnected does not get the message, ever. It is the right primitive for live dashboards, presence pings, and ephemeral fan-out. It is the wrong primitive for anything that must not be lost.
A LIST with LPUSH and BLPOP is a simple at-least-once work queue. Producers push, consumers block-pop. The message lives in the list until a consumer pops it, and the worker can implement manual ACK by holding the message in a "processing" list and LREM-ing it on success. This is the right primitive for background jobs and simple task queues where one message goes to one worker.
Streams are the heavyweight option: a durable log with IDs, consumer groups, explicit XACK, and a Pending Entries List that tracks delivered-but-unacked messages. Streams give you replay by ID, the ability to add a consumer group years after the fact, and a recovery path for crashed consumers. They cost more memory and more operational thought, and they are the only option when you need at-least-once with consumer groups inside Redis.
The production failure I keep telling people about is a team that picked Pub/Sub for an order-events channel because "Streams looked complicated and we just need fan-out." The events drove billing, shipping, and an analytics aggregator. Things worked for six months. Then they deployed a new version of the analytics service on a Tuesday afternoon. The rolling deploy took three hours because of a health-check misconfiguration. Every order-event published during those three hours reached billing and shipping, which were not restarting, but never reached analytics. Three hours of orders were never billed for upsells, because the upsell rule lived in the analytics consumer. The bug was not noticed for two days, and reconciliation took a week.
The fix was to migrate to Streams with a consumer group per downstream service. A consumer that restarts now picks up exactly where it left off, because its position is stored in the stream's consumer group state. Pub/Sub stayed in place for the live dashboard fan-out, where losing a tick during a deploy genuinely does not matter.
Pick the primitive that matches the cost of a lost message. Redis will happily lose messages for you if you ask it to.
Pub/Sub is fire-and-forget by design. If you need durability or replay, use Streams. If you need a simple work queue, use a LIST with BLPOP. Mixing them up is how messages quietly disappear.
Originally posted on LinkedIn. View original.