Messaging delivery semantics comparison

At-least-once vs exactly-once delivery

Every message queue and event stream has to make a promise about what happens when a network hiccup, a crash, or a slow acknowledgement gets in the way. "Exactly-once" sounds like the obvious best choice — but it's the hardest, most misunderstood guarantee in distributed systems, and knowing what it actually means (and doesn't) prevents costly bugs.

TL;DR

  • At-least-once guarantees no message is ever lost, at the cost of possible duplicates — the sender keeps retrying until it gets confirmation.
  • Exactly-once is a stronger guarantee that the end effect of processing looks like it happened one time — achieved through idempotency or transactional consumption, not through the transport magically never duplicating.
  • Most production systems are "at-least-once + idempotent consumer," which behaves like exactly-once without needing perfect delivery.

Side-by-side comparison

AspectAt-least-onceExactly-once
Message lossNever (sender retries until acknowledged)Never (built on top of at-least-once)
Duplicates possible?Yes — the defining trade-offNo observable duplicates, thanks to idempotency/transactions
Complexity to implementLow — retry until ackHigh — needs idempotency keys or transactional offsets
Consumer responsibilityMust tolerate/dedupe duplicates itselfFramework or app logic guarantees no double-processing
Performance costLow overheadHigher — transactions, dedup lookups add latency
Common examplesMost webhooks (Stripe, GitHub), SQS standard queues, RabbitMQ defaultKafka transactions (idempotent producer + read-committed), Flink checkpointing
Failure mode if ignoredDuplicate side effects — double-charged payment, duplicate emailN/A if implemented correctly — but adds real engineering cost
Default choice for new systemsYes — simpler, and idempotency is needed anyway for retries/timeoutsOnly when duplicates are genuinely unacceptable and can't be handled downstream

Code side-by-side

Processing a "charge customer" event safely under each model:

At-least-once — naive (buggy)

// Consumer may receive this event TWICE
onMessage("ChargeCustomer", (event) => {
  paymentGateway.charge(
    event.customerId, event.amount
  );
  // No dedup -> customer charged twice
  // if the ack was lost and the
  // message was redelivered
});

At-least-once + idempotency = "effectively exactly-once"

onMessage("ChargeCustomer", (event) => {
  // event.id is a unique, stable key
  if (processedIds.has(event.id)) {
    return; // already handled -> skip
  }
  paymentGateway.charge(
    event.customerId, event.amount,
    { idempotencyKey: event.id } // gateway
    // also dedupes on its own side
  );
  processedIds.add(event.id);
});

When at-least-once is the right (and usual) choice

  • You are building or consuming webhooks. Every major webhook provider delivers at-least-once and expects receivers to dedupe on the event ID — fighting this is pointless; design for it.
  • Losing a message is far worse than processing it twice. Order confirmations, notifications, and audit events are usually safe to occasionally duplicate but never safe to drop.
  • You need low operational complexity and high throughput. At-least-once with a simple retry loop is cheap to build and reason about compared to distributed transactions.
  • Your downstream operations are naturally idempotent. If "set status to SHIPPED" runs twice, nothing breaks — you don't need exactly-once machinery to be safe.

When you need exactly-once semantics

  • Duplicate processing causes real financial or data-integrity damage. Incrementing a balance, decrementing inventory, or issuing a single physical action (print a check, ship a package) needs strict duplicate protection.
  • You're building a stream-processing pipeline with aggregations. Kafka Streams / Flink exactly-once semantics prevent double-counting in running totals, which naive at-least-once consumption would silently corrupt.
  • You can afford the operational and latency cost. Transactional consumption and offset commits add real overhead — worth it only when the alternative (custom idempotency logic everywhere) is riskier than the framework cost.
  • Downstream systems are not naturally idempotent and can't easily be made so. If dedup keys aren't available or the operation genuinely isn't naturally repeatable, framework-level exactly-once removes that burden from every consumer.

English phrases engineers use

At-least-once conversations

  • "This queue is at-least-once — always design consumers to be idempotent."
  • "We got a duplicate delivery after the consumer's ack timed out."
  • "The webhook retried three times before we returned a 200."
  • "Dedup on the event ID, not on message content."

Exactly-once conversations

  • "We enabled Kafka's exactly-once semantics for this consumer group."
  • "The idempotency key prevents the gateway from double-charging."
  • "This is effectively-once, not truly exactly-once at the network layer."
  • "We commit the offset in the same transaction as the write, so there's no gap."

Quick decision tree

  • Building or consuming webhooks → At-least-once + idempotency key dedup
  • Operation is naturally idempotent (set-status, upsert) → At-least-once is enough
  • Operation is a raw increment/decrement (balance, inventory count) → Need exactly-once processing or make it idempotent (upsert-style)
  • Stream aggregation / running totals → Exactly-once processing (Kafka transactions, Flink checkpoints)
  • Team is new to messaging and timeline is tight → Start at-least-once; add idempotency, not full exactly-once machinery
  • Regulatory requirement for "processed exactly once, provably" → Exactly-once with audited idempotency keys

Frequently asked questions

Is "exactly-once" delivery actually possible?

True exactly-once network delivery is not possible in a distributed system with unreliable networks — you can never be 100% certain a message was received without an acknowledgement, and acknowledgements can themselves be lost, which forces a retry that may duplicate the message. What systems like Kafka actually provide is "exactly-once processing" or "effectively-once semantics": at-least-once delivery combined with idempotent writes or transactional offsets, so the end result looks exactly-once even though a message might physically be delivered more than once.

What is the difference between at-least-once and at-most-once?

At-least-once guarantees a message is never lost — the sender retries until it gets an acknowledgement, which can cause duplicates. At-most-once guarantees a message is never duplicated — the sender does not retry, which can cause message loss if the first attempt fails silently. At-least-once is the far more common choice because losing data is usually worse than having to deduplicate it.

How do you achieve exactly-once processing in practice?

The standard technique is idempotency: design the consumer so that processing the same message twice produces the same result as processing it once. Common patterns are using a unique message ID and an "already processed" table (idempotency key), using upserts instead of increments (SET balance = 100 instead of balance += 10), or using the messaging system's built-in transactional guarantees (Kafka's exactly-once semantics ties message consumption, processing, and offset commit into one atomic transaction).