jonnifyredis
Redis Patterns / R3 · Reliable Queues / At-least-once / Why exactly-once is a lie

R3.02 · dive 3

Why exactly-once is a lie

The acknowledgement can be lost. A queue that picks no-loss must redeliver on a missing acknowledgement, and no protocol closes that gap — so exactly-once delivery is unreachable.

Exactly-once delivery is sold as a feature; it is not one. The acknowledgement that would confirm a single delivery can itself be lost, and “the work never ran” and “the acknowledgement was lost” leave the queue in the same state — indistinguishable from the outside. Drop the acknowledgement on the run to the right and watch the redelivery become unavoidable. What you build instead is an exactly-once effect: at-least-once delivery on an idempotent consumer.

Worker → queue · drop the ack · the redelivery is unavoidable

worker did the work queue ack seen → remove job ACK × ack arrived · the queue removes the job · one effect, clean no ambiguity: the queue has the acknowledgement on record

The acknowledgement gap

The argument is the two-generals problem in queue clothing. A worker takes a job, does the work, and sends an acknowledgement. The queue removes the job when the acknowledgement arrives. Now ask: what happens if the acknowledgement is lost in transit, or the worker crashes the instant after sending it.

The queue is in a position with no clean exit. It sent the job and received no acknowledgement. Two histories produce that exact state, and from the outside the two are indistinguishable:

The work never ran (a real loss), so the job must be redelivered. Or the work ran and the acknowledgement was lost, so redelivering runs the work twice.

To guarantee no loss, the queue must redeliver on a missing acknowledgement. But redelivering on a missing acknowledgement runs the second history twice. There is no third behaviour: redeliver and risk a duplicate, or do not redeliver and risk a loss. A confirmation of the confirmation does not help — that message can be lost too, and the regress never bottoms out. The gap is structural, not an implementation gap.

worker:  take job → do work → send ACK ──╳ (ACK lost here)
queue:   sent job, no ACK on record → must redeliver to avoid loss → work runs twice
         # "work never ran" and "ACK was lost" leave the same state

So a queue picks a side. A reliable queue picks no-loss, which forces redeliver-on-missing-ack, which is at-least-once. The duplicate is the price of never losing a job, and it is paid at the acknowledgement gap.

Two histories produce the same “no acknowledgement on record” state, indistinguishable from the outside; choosing no-loss forces redeliver-on-missing-ack, which is at-least-once.

Exactly-once effect is the real target

Exactly-once delivery is unreachable; exactly-once effect is reachable, and it is what the phrase should mean. Hold at-least-once delivery — never lose a job — and make the consumer idempotent so a redelivery changes nothing the second time. The effect happens once even though the delivery may happen twice. Systems that advertise “exactly-once” deliver this: at-least-once transport plus an idempotent or deduplicating consumer. The honest framing moves the guarantee from the delivery to the effect, where it can actually hold.

This reframing is not a downgrade. It is more honest and it is more robust, because it puts the duplicate-handling where the duplicate is observable — at the effect, keyed by the job's identity — rather than pretending a transport layer can resolve a lost acknowledgement.

Move the guarantee from the delivery to the effect: at-least-once delivery plus an idempotent consumer is the exactly-once that actually holds.

Two keys, two jobs

The sharpest error in this area is to point at a deduplication key and call it exactly-once. EchoMQ's EchoMQ.Keys.dedup/2 builds emq:{queue}:de:{id} ("#{base(ctx)}:de:#{dedup_id}"), and removeDeduplicationKey-1.lua cleans it up — it GETs the key and DELs it only when the value matches the job id. This is real and useful, and it is producer-side: it dedups at enqueue, dropping a second job that carries the same deduplication id before it ever enters the queue.

That is a different job from consumer exactly-once. Producer dedup answers “two identical jobs were submitted — keep one.” Consumer idempotency answers “one job was delivered twice by the recovery path — apply its effect once.” The recovery sweep can redeliver a single in-flight job whose lock expired regardless of the producer dedup marker, because that job already entered the queue exactly once. So the de:{id} marker does nothing to close the acknowledgement gap at the consumer.

Two keys, two moments · pick which job to protect

producer-side · at enqueue emq:{queue}:de:{id} keyed by the dedup id stops duplicate submissions consumer-side · at effect-time dedup:{job_id} (SET NX) keyed by the job's identity absorbs duplicate deliveries producer dedup is not consumer exactly-once · two keys protect two moments
# producer-side dedup key — real, and NOT consumer exactly-once
def dedup(ctx, dedup_id), do: "#{base(ctx)}:de:#{dedup_id}"   # emq:my_queue:de:<id>, dedups at enqueue

Producer dedup keeps one of two identical submissions; consumer idempotency applies one delivery's effect. The de:{id} marker is not the answer to “is my consumer exactly-once” — an idempotent consumer is.

In EchoMQ — the dedup key is producer-side

The grounding is precise, and it is the discipline of this whole module. EchoMQ's EchoMQ.Keys.dedup/2 is a real, producer-side key — it dedups submissions at enqueue and is cleaned by removeDeduplicationKey-1.lua. It is not, and does not claim to be, consumer exactly-once. The exactly-once effect a reliable queue ships is the consumer's, made by an effect-time NX marker or a naturally idempotent effect, as the previous dive built.

the pattern

Exactly-once delivery is impossible because the acknowledgement can be lost; build an exactly-once effect on at-least-once delivery and an idempotent consumer.

in EchoMQ

The producer-side EchoMQ.Keys.dedup/2 marker dedups submissions at enqueue and is not consumer exactly-once; the exactly-once effect is the consumer's, made by an effect-time NX marker or a naturally idempotent effect.

EchoMQ's de:{id} key is real and producer-side; the exactly-once effect is the consumer's, and no key on the producer makes the delivery exactly-once.

Door · not depth The full recovery coordination — the heartbeat manager, the lock TTL, the stalled-check across a worker pool — is the dedicated EchoMQ course. From here, R3.02 opens onto E6 · the lifecycle — the acknowledge/finish lifecycle. This dive teaches why exactly-once delivery cannot exist; that course teaches the lifecycle that makes the effect exactly-once anyway.

References

The command behind the consumer-side exactly-once effect, the queue's delivery-guarantee framing, the reliable-queue design note, and the protocol EchoMQ ports.

Sources

Related in this course