Redis Streams Are At-Least-Once. You Build Exactly-Once Effects on Top.

January 16, 2026


Every time someone asks "does Redis Streams give me exactly-once," the honest answer is no. What it gives you is durable at-least-once delivery with explicit acks, and that is enough to build exactly-once effects if you do the bookkeeping yourself.

Start with what is actually guaranteed. Producers append entries with XADD. Entries land in the stream log with monotonic IDs and survive crashes as long as Redis persistence is on. Consumers join a group with XREADGROUP. When Redis hands an entry to a consumer, it also moves the ID into the group's Pending Entries List (PEL). That PEL is the contract: the entry is delivered but not yet acknowledged. Only XACK removes it.

Now the failure mode that everyone walks into. Consumer C1 reads entry ID 1700-0, applies the side effect (insert a row, send an email, charge a card), then crashes before XACK. The PEL still shows ID 1700-0 as pending. After the idle timeout, another consumer runs XCLAIM (or XAUTOCLAIM) and picks it up. C2 applies the side effect again. You just charged the card twice.

The fix is not "make delivery exactly-once." That is impossible across a network. The fix is to make the effect idempotent. The pattern that survives production is one atomic boundary per message, usually a Lua script or a small transaction that does three things together: check a dedupe set keyed by the stream ID, apply the side effect (typically a database write with a UNIQUE constraint on the same ID), and XACK the entry. If the dedupe set already has the ID, skip the write and ack anyway. Duplicate delivery, no duplicate effect.

The dedupe set is the load-bearing piece. Redis Set with TTL works. A processed_ids table with a unique index works. Whatever you pick, the write that produces the user-visible effect and the write that records "I processed this ID" must be in the same transaction. Otherwise you get a window where the effect happened but the dedupe record did not, and the next claim does it again.

How is this different from Kafka transactions? Kafka can offer end-to-end exactly-once between a Kafka source and a Kafka sink because the broker participates in the transaction (read offsets, produce records, commit, all atomic). Redis Streams cannot do that with an arbitrary external sink. So the responsibility moves to the consumer.

Streams give you a durable log, retry through PEL, and the primitives to build correctness. The correctness itself you write yourself, one Lua boundary at a time.

Key takeaway

Streams guarantee delivery, not uniqueness. Exactly-once effects come from an idempotent sink: dedupe set plus unique-constraint write plus XACK, all inside one Lua boundary.

Originally posted on LinkedIn. View original.


All Rights Reserved.