System Design · Asynchronous Communication
Delivery Guarantees: At-Least-Once vs. At-Most-Once vs. Exactly-Once
Exactly-once delivery is a myth the network can't keep. Here's what messaging systems actually guarantee, and how to build correctness on top of it anyway.
Every message queue, event bus, and RPC framework eventually forces you to answer the same uncomfortable question: what happens when a message gets lost, or a network call times out, or a consumer crashes mid-processing? The answer is one of three delivery guarantees, and picking the right one — and building correctly on top of it — is one of the most consequential decisions in a distributed system's design.
Three guarantees, precisely defined
At-most-once delivery means a message is sent one time and never retried. If it's lost in transit, dropped by a crashed broker, or the consumer never receives it, it's simply gone. The upside is that duplicates are impossible by construction. The downside is equally absolute: silent data loss is a normal, expected outcome, not an edge case.
At-least-once delivery flips the trade-off: the sender keeps retrying until it gets confirmation the message was received, which means a message is never silently lost. But that same retry logic means the same message can arrive twice, or ten times, if an earlier acknowledgment was itself lost or delayed. Nothing disappears, but things can duplicate.
Exactly-once delivery is the guarantee everyone wants: never lose a message, never duplicate it. It sounds like a strictly better version of the other two, and that intuition is exactly what makes it dangerous — because true exactly-once delivery across an unreliable network is, in the general case, provably impossible.
Why exactly-once is a myth
The problem isn't engineering effort — it's the physics of distributed systems. A producer sends a message and starts a timer. If the timer expires with no acknowledgment, the producer has no way to distinguish between two completely different failures: the message never arrived, or the message arrived and was processed fine, but the acknowledgment got lost on the way back. This is a variant of the classic Two Generals' Problem — no amount of retrying can resolve that ambiguity with certainty over an unreliable channel, because every acknowledgment is itself a message that can be lost.
Faced with that ambiguity, the producer has exactly two options: retry (risking a duplicate) or don't (risking data loss). There is no third option that guarantees both delivery and uniqueness at the transport level. What the industry actually ships under the marketing label "exactly-once" is something more modest and more honest: effectively-once processing — at-least-once delivery at the network layer, combined with idempotent handling at the application layer so that duplicates, even though they arrive, produce no additional effect.
Effectively-once, not exactly-once
Kafka's transactional producers, SQS's FIFO queues with deduplication IDs, and Stripe's idempotency keys all take the same shape: retry aggressively at the transport level, then collapse duplicates deterministically at the application level. The guarantee lives in your code, not in the wire protocol.
Why at-least-once plus idempotency is the pragmatic default
Given that exactly-once at the transport level isn't on the menu, the choice narrows to at-most-once or at-least-once — and for almost every system that matters, at-least-once wins. Losing a payment event, an order confirmation, or a sensor reading is usually far more damaging than processing it twice, and duplicates are a problem you can solve deterministically in your own code, while lost messages are a problem you often can't recover from at all. At-most-once is the right choice only when a dropped message is cheap to lose and a duplicate would be actively harmful with no easy way to detect it — think best-effort metrics or live cursor positions, not money movement.
So the practical default becomes: guarantee at-least-once delivery, then make every consumer idempotent so that processing the same logical operation twice is indistinguishable from processing it once.
Where the guarantees actually break
Two moments in the message lifecycle are where theory meets reality:
- The ambiguous producer timeout. A producer sends a message, the broker persists it and replies, but the reply is lost to a network blip. The producer's timer expires with no confirmation, so it retries — and now the broker has the message twice, even though nothing on the producer side technically failed.
- The crash between processing and acknowledging. A consumer pulls a message, does the work — charges a card, sends an email, writes a row — and crashes before it can send the acknowledgment back to the broker. The broker, having never heard back, redelivers the same message to another consumer, which dutifully does the work all over again.
Notice that both failures happen precisely because the system is trying to guarantee at-least-once delivery. The retry that prevents message loss is the same retry that creates the duplicate — the two are inseparable without extra machinery on the consumer side.
Idempotency keys and dedup windows
The concrete mechanism that closes the gap is an idempotency key: a unique identifier attached to each logical operation — not each network attempt — so that retries of the same operation carry the same key. The consumer checks a durable store for that key before doing any work; if it's already there, the operation is skipped as a known duplicate, and if it isn't, the consumer does the work and records the key atomically with (or immediately after) the side effect.
def handle_payment(event):
key = event.idempotency_key # unique per logical operation
# Check-then-act must be a single atomic step in the store,
# not two separate calls (that's its own race condition).
already_done = dedup_store.get_and_set(key, ttl="24h")
if already_done:
return # seen this exact operation before — skip safely
charge_card(event.amount, event.account)
dedup_store.mark_complete(key)Two details make this reliable rather than merely comforting. First, the check-and-record step has to be atomic — checking for the key and then separately writing it introduces a new race where two concurrent retries both see "not found" and both proceed. Second, most systems keep keys only for a bounded dedup window — hours or days, not forever — since retries that arrive weeks late are rare enough to accept the small residual risk in exchange for a store that doesn't grow unbounded.
Idempotent at the API, not just in the database
A unique constraint on a database row stops a duplicate write, but it doesn't stop the email or the webhook that already fired before the write was attempted. Idempotency has to be enforced at the boundary of every side effect the operation triggers, not just the one that happens to be easiest to deduplicate.
Choosing deliberately, not by default
The right guarantee is a design decision, not a configuration default left over from whatever the messaging library shipped with. Ask what a lost message costs versus what a duplicate costs, and design the consumer accordingly. In the overwhelming majority of real systems, that answer is at-least-once delivery with an idempotent consumer — accepting a little extra work on the application side in exchange for a correctness guarantee the network itself can never actually provide.
Key takeaways
- At-most-once may lose messages but never duplicates; at-least-once never loses but may duplicate; exactly-once — never losing and never duplicating — isn't achievable over an unreliable network.
- The Two Generals' Problem explains why: a producer can't distinguish 'message lost' from 'ack lost' after a timeout, so it must choose between retrying (risking a duplicate) or not (risking loss).
- What ships as 'exactly-once' in real systems is actually effectively-once: at-least-once delivery plus idempotent processing on the consumer.
- At-least-once plus idempotency is the right default for most systems, since lost data is usually far more costly than a duplicate you can detect and discard.
- Guarantees break at two moments: an ambiguous producer timeout, and a consumer crashing after processing but before acknowledging.
- Idempotency keys tied to the logical operation, checked and recorded atomically within a bounded dedup window, are the concrete mechanism that makes duplicates harmless.
