Skip to content

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.

At-most-once send, never retry → fast, but messages can be LOST
At-least-once send, retry until ack'd → never lost, but can DUPLICATE
Exactly-once each message acted on once, no loss, no dup → the dream

You’re always choosing between losing messages and duplicating them. Exactly-once promises to escape the dilemma — and that’s where the trouble starts.

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 DUPLICATE
World 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 once

Two 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 9f3
    msg{id: 9f3, "credit $10"} (duplicate) → processed? YES → skip
  • Naturally idempotent operations. Design the effect so repetition doesn’t matter: SET balance = 100 is idempotent; ADD 10 to balance is 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.

  • 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.

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.

  1. State the three delivery guarantees and the loss-vs-duplicate trade-off between them.
  2. Explain, using the lost-ack scenario, why exactly-once delivery is impossible.
  3. What do systems usually mean when they advertise “exactly-once”?
  4. How do at-least-once delivery and idempotent processing combine to give effectively-once?
  5. Why is SET balance = 100 idempotent while ADD 10 to balance is not, and why does that matter here?