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
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 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.
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.
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.
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
- Redis — SET — the
NXmarker behind the consumer-side exactly-once effect. - Redis — Redis queue — the queue and its delivery-guarantee framing.
- Salvatore Sanfilippo — RPOPLPUSH (the reliable queue pattern) — the design note for the reliable-queue move, by the creator of Redis.
- BullMQ — the queue protocol — the worker path EchoMQ ports, including custom ids and deduplication.
Related in this course
- R3.02 · At-least-once — the module hub.
- R3.02.1 · At-least-once semantics — the three guarantees and the structural duplicate.
- R3.02.2 · Idempotent consumers — the exactly-once effect, built.
- R3 · The reliable queue — the family in one place.
- R2.01 · Atomic updates — the atomic
SET … NXclaim.