What Do At‑most‑once, At‑least‑once, And Exactly‑once Delivery Semantics Mean
Exactly-once delivery is impossible — start there
Over an unreliable network, exactly-once delivery cannot be achieved. This is not an engineering gap that a better broker will one day close; it is the Two Generals problem. A sender transmits a message and waits for an acknowledgement. If no ack arrives, the sender is stuck in a fundamental ambiguity: did the message get lost, or did the message arrive and the ack get lost? It cannot tell the two apart. So it has exactly two choices, and each buys one guarantee at the cost of the other:
- Don't retry → at-most-once. Never a duplicate, but the message may be lost.
- Retry → at-least-once. Never lost, but the message may be duplicated (you retry a message that actually arrived).
There is no third delivery guarantee. What you can build — and what every serious system builds — is effectively-once:
at-least-once delivery + idempotent processing = effectively-once. Stop trying to deliver exactly once. Retry until it lands, and make duplicates harmless by deduplicating on the consumer.
The distinction that trips people up: exactly-once delivery (the wire guarantee) is a myth; exactly-once effect (the observable outcome) is achievable. The legacy framing that opens with "each message arrives exactly one time, no loss and no duplicates" describes the myth as if it were a product you can buy. It isn't. Everything below is about producing the effect honestly.
The three semantics, by mechanism
All three differ in one decision: when do you acknowledge relative to processing, and do you retry? That single choice determines the failure mode.
At-most-once — ack (or don't wait) before processing
Fire-and-forget. The sender transmits and moves on without waiting for confirmation, or the consumer commits the offset / acks before doing the work. If anything crashes mid-flight, the message is gone and nobody retries. Zero duplicate risk, zero retry overhead, lowest latency — and silent loss. Correct for high-volume telemetry, metrics, and best-effort notifications where one dropped sample out of millions is invisible.
At-least-once — ack after processing, retry on timeout
The sender keeps the message until it receives an ack, and retransmits if the ack doesn't arrive within a timeout. The consumer commits its offset / acks only after the work is durably done. Nothing is ever lost. But the lost-ack case is now a guaranteed source of duplicates: the work completed, the ack died in transit, the sender retries, the consumer sees the same message again. This is the default posture of RabbitMQ acks and SQS visibility timeouts, and what a Kafka consumer gets once it commits offsets after processing — note that Kafka's out-of-the-box enable.auto.commit=true commits on a 5-second timer decoupled from your processing, which can also lose messages on a crash; turn it off (manual commit after the work) for true at-least-once. Either way the trade holds because losing data is usually worse than seeing it twice.
Effectively-once — at-least-once plus a dedup key or idempotent write
Keep at-least-once delivery for durability, then neutralize the duplicates at the point of effect. Two techniques:
- Dedup key / idempotency key: attach a stable unique id to each logical operation. The consumer records "key → result"; a second arrival with the same key returns the stored result instead of re-doing the work.
- Idempotent write: shape the operation so that repeating it changes nothing.
SET balance = 100,PUT user.email = x, andINSERT ... ON CONFLICT DO NOTHINGagainst a unique transfer id are naturally idempotent;balance += 50, "append", and "charge the card" are not.
Traced example: a lost ack, step by step
A producer submits "charge $50, key=abc". The consumer charges the card and stores the result under abc. The ack back to the producer is lost in the network. The producer's timeout fires; it cannot distinguish "never processed" from "processed, ack lost", so — being at-least-once — it retries the identical message. Without dedup, the card is charged twice. With a dedup table keyed on abc, the second arrival is recognized and the stored result is returned. One effect, despite two deliveries.
| t | Producer | Network | Consumer | Effect so far |
|---|---|---|---|---|
| 1 | send charge $50, key=abc | msg delivered | receives msg | — |
| 2 | waiting for ack | — | charge card; store abc → "OK $50" | charged once |
| 3 | waiting for ack | ack LOST | ack sent | charged once |
| 4 | timeout → ambiguity: lost or ack-lost? | — | idle | charged once |
| 5 | RETRY same msg, same key=abc | msg delivered | receives msg again | charged once |
| 6a | — | — | no dedup: charge again | charged TWICE ✗ |
| 6b | — | — | dedup: key abc seen → return stored result | charged once ✓ |
| 7 | receives ack | ack delivered | — | done |
The dedup check must be atomic — a naive "check if key exists, then act" is itself a race between two concurrent retries. Use an atomic putIfAbsent or a database unique constraint so the second writer loses cleanly.
Kafka's "exactly-once semantics" (EOS), precisely
Kafka advertises exactly-once. It is real, it is well-engineered, and it is bounded. EOS is two independent mechanisms, and knowing exactly what each does — and where the boundary ends — is the staff-level distinction.
1. The idempotent producer (dedups producer retries)
When a producer's send ack is lost, it retries — the classic duplicate source, but now on the write path into the log. Kafka assigns each producer a PID (producer id) and stamps every record with a monotonic sequence number per partition. The broker tracks the last sequence it accepted for that (PID, partition). A retry carries the same sequence number, so the broker recognizes it as a duplicate and drops it without appending a second copy. This deduplicates producer retries within a single partition — it does not span partitions and does not touch the consumer side. Enabled with enable.idempotence=true (default in modern Kafka).
2. Transactions (atomic read-process-write)
The idempotent producer stops duplicate writes. Transactions solve the stream-processing pattern "consume from topic A, produce to topic B, commit the consumed offset" — making the produced records and the offset commit a single atomic unit. Either all of it commits or none of it does. A transactional.id gives the producer a stable identity so a fenced-off zombie after a crash cannot commit. The final piece is on the consumer: isolation.level=read_committed, so downstream readers only ever see records from committed transactions and never observe the aborted ones. Together this is genuine exactly-once processing for a Kafka-to-Kafka pipeline.
The boundary: Kafka EOS holds within Kafka's world — records in topics, offsets in the __consumer_offsets topic, state in a Kafka-backed store. The instant your consumer performs an external side effect — charge a card, send an email, call a third-party API, write to a non-transactional database — that effect is outside the transaction and Kafka guarantees nothing about it. An email can't be un-sent. The transaction can abort and roll back the offset, but the email is already gone. External effects still need their own idempotency key.
Pitfalls
- Believing the broker gives you end-to-end exactly-once. "We use Kafka EOS, so we can't double-charge" is false. EOS ends at the boundary of Kafka; your card charge is an external effect and needs its own idempotency key. The broker guarantee and the business guarantee are different guarantees.
- Dedup window / TTL too short. A dedup table with a 1-hour TTL cannot catch a retry that arrives after a 3-hour broker outage or a delayed redelivery. The retention must exceed the maximum realistic retry horizon, or a late duplicate slips through as a fresh operation.
- Non-idempotent side effects.
balance += 50, appending to a list, or POSTing to a payment API are not safe to repeat. At-least-once delivery will repeat them. Either make them idempotent (absolute set, unique constraint) or gate them with a key. - Assuming ordering implies dedup. Kafka's per-partition ordering guarantees records are read in the order written; it says nothing about duplicates. Ordered and deduplicated are orthogonal — you can have in-order duplicates.
- Non-atomic dedup check. "SELECT then INSERT" lets two concurrent retries both pass the check. Use a unique constraint or atomic compare-and-set so exactly one wins.
Selection & trade-offs
Three real options, ranked by cost. Pick the cheapest one that meets the failure tolerance of the data.
| Approach | Loss? | Duplicates? | Latency / throughput | Complexity | Use when |
|---|---|---|---|---|---|
| At-most-once | possible | never | best (no acks, no retries) | lowest | metrics, telemetry, logs, best-effort notifications — a dropped sample is invisible |
| At-least-once + idempotent processing (effectively-once) | never | on the wire, but neutralized in effect | moderate (acks + retries + a dedup read/write) | moderate — you own the dedup key and TTL | the default for anything that matters: payments, orders, queue consumers, webhooks. Works across arbitrary external systems. |
| Kafka EOS (idempotent producer + transactions) | never | none within Kafka | worst of the durable options — transactions add a commit round per batch (transaction markers + coordinator write), cutting throughput and adding end-to-end latency vs plain at-least-once; tune the transaction commit cadence (commit.interval.ms in Kafka Streams) to trade latency for batching — transaction.timeout.ms is only the abort deadline for hung transactions, not a batching dial | highest config surface, but the dedup logic is the broker's, not yours | Kafka-to-Kafka stream processing (read-process-write) where the whole effect lives inside Kafka. Does not extend to external side effects. |
The decision rule: if losing data is acceptable, use at-most-once and stop paying for reliability you don't need. If the effect is entirely inside Kafka, EOS gives you exactly-once processing with the dedup handled for you. For everything else — and especially anything touching an external system — at-least-once + idempotency is the honest, portable answer. Note that even a Kafka EOS pipeline that reaches out to charge a card degrades to "at-least-once + your own idempotency key" at that reach-out point; EOS does not rescue you there.
Takeaways
- Exactly-once delivery is impossible (Two Generals): the sender can't tell a lost message from a lost ack, so it either risks loss (at-most-once) or risks duplicates (at-least-once). The achievable goal is exactly-once effect.
- Effectively-once = at-least-once + idempotent processing. Retry for durability; make duplicates harmless with a dedup key or an idempotent write. Keep the dedup check atomic and the retention window longer than your retry horizon.
- Kafka EOS is idempotent producer (PID + per-partition sequence) plus transactions (atomic read-process-write,
read_committed) — genuine, but bounded to Kafka's own world. - External side effects (charge, email, third-party call) are outside every broker's exactly-once guarantee. An email can't be un-sent — carry your own idempotency key across that boundary.
Sources & further reading. Kleppmann, Designing Data-Intensive Applications (ch. 8–9, "The Trouble with Distributed Systems" and consistency/consensus, on why exactly-once delivery is unattainable and how idempotence recovers the effect). Kafka exactly-once design: KIP-98 — Exactly Once Delivery and Transactional Messaging, and the Confluent "Exactly-once Semantics" design notes (idempotent producer, transactions, read_committed). The Two Generals problem (impossibility of coordinated agreement over a lossy channel). Nygard, Release It! (retries, timeouts, and idempotency as production stability patterns). See also this guide: Idempotency & "Exactly-Once Is a Myth", The 8 Fallacies of Distributed Computing (#1), Designing for Failure, Kafka.
🤖 Don't fully get this? Learn it with Claude
Stuck on What Do At‑most‑once, At‑least‑once, And Exactly‑once Delivery Semantics Mean? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **What Do At‑most‑once, At‑least‑once, And Exactly‑once Delivery Semantics Mean** (System Design) and want to truly understand it. Explain What Do At‑most‑once, At‑least‑once, And Exactly‑once Delivery Semantics Mean from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **What Do At‑most‑once, At‑least‑once, And Exactly‑once Delivery Semantics Mean** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **What Do At‑most‑once, At‑least‑once, And Exactly‑once Delivery Semantics Mean** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **What Do At‑most‑once, At‑least‑once, And Exactly‑once Delivery Semantics Mean** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.