Exactly-Once Semantics (and the Myth)
“Exactly-once delivery” is the most requested and most misunderstood guarantee in distributed systems. Engineers ask for it; vendors advertise it; and at the level of message delivery over a network, it cannot exist. Understanding why — and what you can have instead — is a senior-level distinction that prevents a lot of broken designs.
The three delivery guarantees
Section titled “The three delivery guarantees”At-most-once send, never retry → fast, but messages can be LOSTAt-least-once send, retry until ack'd → never lost, but can DUPLICATEExactly-once each message acted on once, no loss, no dup → the dreamYou’re always choosing between losing messages and duplicating them. Exactly-once promises to escape the dilemma — and that’s where the trouble starts.
Why exactly-once delivery is impossible
Section titled “Why exactly-once delivery is impossible”The culprit is the unreliable network plus the two-generals reality: after sending a message, the sender either gets an acknowledgment or it doesn’t. If the ack doesn’t arrive, the sender cannot tell which of two worlds it’s in:
World A: message was delivered, but the ACK was lost → if I retry, that's a DUPLICATEWorld B: message was never delivered → if I DON'T retry, that's a LOSS The sender cannot distinguish A from B. Any fixed choice is wrong in one of them.To never lose, you must retry on missing ack → which duplicates in World A. To never duplicate, you must not retry → which loses in World B. No protocol escapes this; it’s fundamental, the same wall the dual-write problem runs into.
The real solution: at-least-once + idempotency
Section titled “The real solution: at-least-once + idempotency”Since you can’t prevent duplicates, make duplicates harmless. Choose at-least-once delivery (never lose), then design the consumer so that processing the same message twice has the same effect as processing it once — i.e. make it idempotent (see Idempotency).
at-least-once delivery (may duplicate) + idempotent processing (dedup) = effectively onceTwo common ways to achieve idempotent processing:
-
Dedup by ID. Stamp each message with a unique ID; the consumer records processed IDs and skips any it has already seen.
msg{id: 9f3, "credit $10"} → processed? no → apply, record 9f3msg{id: 9f3, "credit $10"} (duplicate) → processed? YES → skip -
Naturally idempotent operations. Design the effect so repetition doesn’t matter:
SET balance = 100is idempotent;ADD 10 to balanceis not. Upserts, “set to state X,” and conditional writes are your friends.
The dedup store has a cost — it must be durable and fast, and you must decide how long to remember IDs (a window long enough to outlast all retries). That’s the price of effectively-once.
Where this bites in practice
Section titled “Where this bites in practice”- Payment systems lean on this entirely: an idempotency key turns inevitable retries into a single charge (see Design a Payment System).
- Message queues / stream processors (Kafka & friends) offer “exactly-once” features that are, under the hood, at-least-once + transactional dedup/offset commits — effectively-once within the system.
- Webhooks are at-least-once by nature; any webhook consumer must dedup or it will double-process.
Under the hood — how Kafka’s “exactly-once” actually works
Section titled “Under the hood — how Kafka’s “exactly-once” actually works”Kafka’s exactly-once is the clearest real example of “effectively-once = at-least-once + dedup,” and it has two distinct parts. First, the idempotent producer: each producer gets a producer ID, and every message carries a per-partition sequence number. When a network hiccup makes the producer retry a send, the broker sees a sequence number it has already written and silently drops the duplicate — so producer retries (the cause of duplicates) stop creating them at the log.
Second, transactions: a producer can write to several partitions and commit the consumer offsets
it read from inside one atomic transaction, so a consume-process-produce loop either fully happens or
fully doesn’t. Downstream consumers set isolation.level=read_committed to ignore records from
transactions that never committed. Notice what’s not happening: no message crosses the network exactly
once. Under the hood it’s still at-least-once delivery, with broker-side dedup and atomic offset commits
doing the work — “effectively once,” exactly as this page describes, dressed in a production-grade
implementation.
The thread
Section titled “The thread”What does this buy us, and what does it cost? Chasing literal exactly-once delivery buys nothing — it’s a mirage. Accepting at-least-once and investing in idempotent processing + dedup buys real correctness (no loss, no duplicate effect) at the cost of a dedup store and the discipline of designing operations to be replay-safe. The senior move is to stop asking the network for a guarantee it can’t give and instead make your processing indifferent to duplicates.
Check your understanding
Section titled “Check your understanding”- State the three delivery guarantees and the loss-vs-duplicate trade-off between them.
- Explain, using the lost-ack scenario, why exactly-once delivery is impossible.
- What do systems usually mean when they advertise “exactly-once”?
- How do at-least-once delivery and idempotent processing combine to give effectively-once?
- Why is
SET balance = 100idempotent whileADD 10 to balanceis not, and why does that matter here?
Show answers
- At-most-once (send, never retry) is fast but messages can be lost; at-least-once (retry until ack’d) never loses but can duplicate; exactly-once (acted on once, no loss, no dup) is the dream. You’re always choosing between losing messages and duplicating them.
- After sending, if the ack doesn’t arrive the sender can’t tell World A (delivered, ack lost — retrying would duplicate) from World B (never delivered — not retrying would lose). To never lose you must retry → duplicates in A; to never duplicate you must not retry → loss in B. No protocol escapes this dilemma, so exactly-once delivery is impossible.
- They almost always mean effectively-once processing within their own boundary — at-least-once delivery combined with deduplication — not a message magically crossing the network exactly once. The guarantee lives in the processing, not the delivery.
- At-least-once guarantees no message is ever lost (you retry), and idempotent processing makes handling the same message twice have the same effect as once (so duplicates are harmless). Together they yield effectively-once: no loss, no duplicate effect.
SET balance = 100is idempotent — applying it twice leaves the same state — so a duplicate delivery is harmless.ADD 10 to balanceis not — applying it twice adds 20 — so a duplicate corrupts the result. This matters because idempotent (or dedup-by-ID) operations are exactly what make at-least-once delivery safe; non-idempotent ones double-count on every retry.