Delivery Guarantees: At-Most-Once, At-Least-Once and Exactly-Once

Message delivery semantics explained: at-most-once, at-least-once and effectively-once processing, duplicates, and how to handle them.

Intermediate 4 min read · Updated

Networks drop messages and machines crash at inconvenient moments, so messaging systems can't promise both "never lose a message" and "never deliver it twice" without help. Delivery semantics describe which failure you accept: at-most-once (may lose), at-least-once (may duplicate) or exactly-once (neither, at a cost and usually only within a closed system).

Where duplicates and losses come from

BrokerConsumerDatabasedeliver msg #81charge card ₹500OKconsumer crashes before ackvisibility timeout → redeliver msg #81charge card ₹500 again?
The consumer processes the message, then crashes before acknowledging. The broker can't tell the difference, so it redelivers: a duplicate.

The core problem: "do the work" and "record that it's done" are two separate steps. Whichever order you do them in, a crash between them causes either a loss or a duplicate.

The three guarantees

GuaranteeHow it's achievedFailure modeFits
At-most-onceAcknowledge (or commit the offset) before processing; never retryMessages can be lostMetrics samples, logs you can afford to drop, live location pings
At-least-onceAcknowledge after processing; retry on failureMessages can be processed more than onceAlmost everything: orders, emails, payments (with idempotency)
Exactly-onceAt-least-once delivery plus deduplication or atomic commit of results and progressHigher cost and complexityFinancial ledgers, billing counts, stream aggregations

Each guarantee, step by step

A consumer reads an event, starts processing, and crashes. Is the event lost, or processed again? It depends on when the consumer acknowledges (commits its offset) relative to doing the work.

At-most-once: fire and forget

BrokerConsumerDatabaseevent Xcommit offset (X done)write error count…crash before the write landsafter restart: next event Y (X is lost)
At-most-once: the offset is committed before processing, so a crash mid-processing loses event X for good.

Each event is processed zero or one time. A logging pipeline commits event X, crashes while writing to the database, restarts after X, and X is gone. Use it when occasional loss is fine and speed matters most: metrics where a few missing points don't change trends, log aggregation with tolerable gaps. Simplest and lowest latency, but loses data on failure.

At-least-once: never lose, may duplicate

BrokerConsumerDatabaseevent Xwrite error count (done)crash before committing the offsetafter restart: event X againwrite again → duplicate unless idempotentcommit offset
At-least-once: the work is done before the commit, so a crash in between causes redelivery and a duplicate write.

Each event is processed one or more times: nothing is lost, but duplicates happen, so downstream must cope:

  • Idempotent operations: an upsert by key gives the same result run once or twice; a plain INSERT duplicates.
  • Deduplication by event ID: store each event's unique ID and skip IDs already seen.
  • Naturally idempotent changes: "set balance to $100" is idempotent; "add $10 to balance" is not.

Most production stream processing uses at-least-once: no data loss, slightly more latency than at-most-once, and duplicates handled in your logic. See idempotency.

Exactly-once: no loss, no duplicates

The processing result and the input acknowledgment commit together atomically: both happen or neither. Flink and Kafka Streams do this with distributed snapshots (checkpoints): periodically capture what's been consumed, what's in flight and what's been output. On failure, restore the last checkpoint and resume; events after it are reprocessed, but their outputs sit in a transactional sink and commit only when the next checkpoint completes, so nothing is emitted twice.

Kafka (input)Stream processorTransactional sinkevents 101–150stage results (uncommitted)checkpoint: offsets 150 + state snapshotcommit staged resultscrash later? restore checkpoint 150, replay 151+, discard uncommitted output
Exactly-once with checkpoints: outputs are staged in a transaction and committed together with the consumed offsets when a checkpoint completes.

The catch: this guarantee usually holds inside the stream processing system. Writing to an external database or API exactly once end to end needs a sink whose transactions coordinate with checkpoints, idempotent writes to the sink, or a way to roll back external writes; otherwise you get exactly-once processing but at-least-once delivery to the sink. Use it where correctness is critical (billing, financial transactions, inventory), accepting checkpoint latency and more complexity.

GuaranteeData lossDuplicatesComplexityLatency
At-most-oncePossibleNoLowLowest
At-least-onceNoPossibleMediumMedium
Exactly-onceNoNoHighHighest

Many production systems choose at-least-once plus idempotent operations, which gives "effectively-once" results with less complexity: upserts instead of inserts, unique event IDs deduplicated at the sink, and naturally idempotent operations. That pushes the complexity into application logic you control rather than distributed transaction protocols that are hard to get right.

Exactly-once is really "effectively once"

Delivering a message exactly once over an unreliable network is impossible in general. What systems actually provide is exactly-once processing effects: the message may arrive twice, but its effect is applied once. Two techniques get there:

  • Idempotent processing: record each message ID (or a business key) with the result, in the same transaction as the side effect, and skip IDs already seen. See idempotency.
  • Atomic commit of output and progress: store the result and the consumer's position together, so they can't diverge. Kafka transactions do this for Kafka-to-Kafka pipelines (consume, produce, and commit offsets atomically); a database consumer can store its offset in the same table transaction as its writes.
BEGIN;
  INSERT INTO processed_messages (message_id) VALUES ('msg-81');   -- fails if seen before
  UPDATE accounts SET balance = balance - 500 WHERE id = 'acct-9';
COMMIT;
-- A redelivered msg-81 hits the unique constraint, the transaction rolls back,
-- and the consumer simply acknowledges it.

Exactly-once inside Kafka doesn't extend to side effects outside it. Sending an email or calling a payment API from a Kafka transaction can still happen twice; those calls need their own idempotency keys.

Producers matter too

Duplicates also appear when a producer retries a publish because the acknowledgement was lost. Kafka's idempotent producer assigns sequence numbers so the broker discards duplicate writes; elsewhere, include a unique event ID so consumers can deduplicate.

Choosing

  • Default to at-least-once delivery with idempotent consumers. It's simple, robust and works with every broker.
  • Use at-most-once only where losing data is genuinely fine and duplicates are worse or pointless.
  • Reach for transactional exactly-once in stream processing where counts and sums must be exact and the whole pipeline is inside one system.

Where it’s used

Payment processing

Consumers record a payment intent ID before charging and pass an idempotency key to the payment provider, so retries never double-charge.

Kafka Streams and Flink

Provide exactly-once state updates by checkpointing state and input offsets atomically.

AWS SQS

Standard queues are at-least-once; FIFO queues add deduplication by message ID within a 5-minute window.

Telemetry

High-volume metrics often use at-most-once UDP or fire-and-forget pipelines because a few lost points don't matter.

In an interview

  • Say "at-least-once with idempotent consumers" and show how deduplication works (message ID table or natural keys).
  • Explain why exactly-once delivery isn't possible but exactly-once effects are.
  • Handle producer-side duplicates too: unique event IDs or idempotent producers.
  • Match the guarantee to the data: losing a metric is fine, losing a payment is not.