At-most-once, at-least-once and exactly-once delivery
Open in the interactive app: practice, quiz, mock interview →In one minute
- Delivery semantics describe how many times a message's effect can happen when producers, brokers and consumers fail: at most once, at least once, or exactly once.
- They are decided by when you acknowledge relative to when you act: ack first gives at most once, act first gives at least once.
- Exactly-once delivery over an unreliable network is impossible; exactly-once effect is achievable by making processing idempotent or transactional.
- Most production systems run at-least-once delivery plus idempotent consumers, which is often called "effectively once".
- The semantics apply per hop; an end-to-end guarantee is only as strong as its weakest hop.
How it works
Why duplicates and losses happen
A producer sends a message and the acknowledgement is lost. It cannot tell whether the broker stored the message, so it must either retry (risking a duplicate) or give up (risking a loss). The same dilemma repeats between broker and consumer: the consumer processes a message and crashes before committing its offset or acking. On restart, the broker redelivers. No protocol over a lossy channel can remove this ambiguity; it can only choose which side to err on. This is the Two Generals' Problem in miniature.
At most once
The consumer commits or acks before processing. If it crashes mid-processing, the message is gone. Producers send without retries. This is acceptable for metrics samples, telemetry and cache invalidation hints where a missing message is cheaper than a duplicate or extra latency.
At least once
The consumer processes then commits. A crash between the two causes redelivery and reprocessing. Producers retry until acknowledged. Nothing is lost, but every downstream effect must tolerate repeats. This is the default in SQS, RabbitMQ with manual acks, and Kafka with commit-after-processing.
Defaults move, so check the version you run. Kafka producers used to default to acks=1 with idempotence off, which could lose a write on leader failover and duplicate one on retry. KIP-679 changed the defaults to acks=all and enable.idempotence=true for Kafka 3.0, but a config-validation bug kept idempotence off until 3.0.1, 3.1.1 and 3.2.0 (KAFKA-13598). It is still switched off silently if you set a conflicting acks, retries or max.in.flight.requests.per.connection without enabling it explicitly (Kafka producer configs). Set it explicitly.
Exactly-once effect
Two techniques get you there:
- Idempotent processing. Give each message a stable id (generated by the producer, not the broker) and record processed ids in the same transaction as the side effect. A redelivered message finds its id and is skipped. Alternatively make the operation naturally idempotent:
SET balance = 50instead ofADD 10, upserts keyed by business id. - Transactions spanning input and output. Kafka's idempotent producer (sequence numbers per partition so the broker drops retried duplicates) plus transactions let a consume-transform-produce job write output records and commit input offsets atomically (KIP-98). Downstream readers with
read_committednever see aborted output. This covers Kafka-to-Kafka only; the moment you call an external API or database, you are back to idempotency.
The outbox problem
A service must update its database and publish an event. Doing both directly risks one succeeding without the other. The transactional outbox writes the event into an outbox table in the same database transaction, and a relay publishes it afterwards, at least once (microservices.io). Consumers deduplicate by event id.
Down the stack
Layer 1The contract
Each system states its guarantee per hop. A Kafka producer with acks=all gets an acknowledgement only after every in-sync replica has the record, and with min.insync.replicas=2 an acknowledged write survives the loss of any one broker; if the in-sync set shrinks below that, the producer gets NotEnoughReplicas instead of a weak ack (Kafka topic configs). A committed consumer offset means "everything before this has been handled"; on restart, consumption resumes exactly there. SQS standard queues promise at-least-once delivery and best-effort ordering; SQS FIFO queues drop a message whose deduplication id was seen in the previous 5 minutes (SQS exactly-once processing).
Read "exactly once" labels closely. Google Pub/Sub's exactly-once delivery promises no redelivery before the ack deadline and none after a successful ack, but a publisher that sends twice still produces two messages with different IDs (Pub/Sub API reference). None of these promises extends past the system boundary to your database or an email provider.
Layer 2The mechanism
Kafka's idempotent producer is assigned a producer id and epoch, and numbers each batch per partition. The partition leader remembers the last five batch sequences per producer, which is why idempotence requires at most 5 in-flight requests per connection; a retried batch with a known sequence is acknowledged without being written again, and a gap raises an out-of-order error. Transactions add a coordinator that logs state in the internal __transaction_state topic. The producer registers partitions with AddPartitionsToTxn, adds the consumed offsets with sendOffsetsToTransaction (so offset commits to __consumer_offsets join the transaction), and on commit the coordinator writes COMMIT or ABORT control markers into every touched partition. A read_committed consumer reads only up to the last stable offset, holding back records from open transactions. On the consumer side, the dedup pattern is a unique index: the insert of the event id fails or succeeds atomically with the business update.
Layer 3The machine
acks=all costs a replication round trip inside the cluster, typically 1 to 3 ms. Kafka does not fsync per message by default; durability comes from replicas holding the data in their page caches, and the OS flushes to disk later. That trade is safe only while replicas fail independently. NATS JetStream makes a similar one, acknowledging at once but flushing about every two minutes by default, and Jepsen showed that a simulated power loss then drops acknowledged writes (Jepsen: NATS 2.12.1). Transactions add control records and coordinator round trips per commit, which is why stream processors commit every 100 ms or so and accept that much extra latency. In PostgreSQL, the dedup insert touches a B-tree index page and appends a WAL record; the commit waits for the WAL fsync, around 0.1 to 1 ms on an SSD, and group commit lets many concurrent transactions share one flush. That is why batching 100 events per transaction raises throughput by an order of magnitude.
Why it's built this way
- Networks lose acknowledgements, so senders retry, and retries create duplicates; no protocol avoids this.
- Machines crash between acting and recording, so the order of "act" and "ack" chooses between loss and duplication.
- A broker can only deduplicate what it can identify, so producers carry ids and sequence numbers.
- External systems cannot join a broker's transaction, so exactly-once effects end up implemented at the sink with idempotency.
- Remembering every id forever is too expensive, so dedup state is windowed, and the window bounds the guarantee.
Diagram
stateDiagram-v2
[*] --> Received
Received --> Processing: at-least-once (act first)
Received --> Committed: at-most-once (ack first)
Committed --> Processing
Processing --> Done: success then commit
Processing --> Crash: failure
Crash --> Received: redelivered (duplicate risk)
Crash --> Lost: offset already committed
Done --> [*]
Lost --> [*]Worked example
A payments ledger consumes 5,000 PaymentCaptured events per second from Kafka and credits merchant balances in PostgreSQL.
Consumers commit offsets every 5 seconds. A consumer crashing just before a commit has processed up to 5 seconds of its partition's events, say 500 events if it owns 10% of traffic, which will all be redelivered to the new owner after rebalance. Without deduplication, 500 merchants get double credits. During a rolling deploy of 20 consumers, that could happen 20 times.
Fix: the consumer runs, per event, a single transaction:
BEGIN;
INSERT INTO processed_events (event_id) VALUES ($1)
ON CONFLICT DO NOTHING; -- 0 rows means duplicate: ROLLBACK and skip
UPDATE balances SET amount = amount + $2 WHERE merchant_id = $3;
COMMIT;The processed_events table grows by 432 million rows a day, so it is partitioned by day and partitions older than 7 days are dropped. That is safe only because Kafka redelivery can never reach back further than the consumer's committed offset plus the commit interval, and producer retries settle within minutes. The extra insert adds about 0.3 ms per event; batching 100 events per transaction keeps throughput well above 5,000 per second.
In the wild
- Jepsen, 2013: Kafka acknowledges writes it then loses. When the in-sync replica set shrank to just the leader, the leader kept acknowledging writes held only on its own disk, and a lagging replica could later be elected leader. In one run 520 of 987 acknowledged writes, about 53%, were lost. Kafka later added
min.insync.replicasand a switch to disable unclean leader election, which is whyacks=allalone is not the whole contract (Jepsen). - Jepsen, 2022: Redpanda duplicates and reorders under retries. With server-side idempotence off by default, producer retries wrote duplicates, and some duplicates appeared even with it on. Uncommitted Raft entries were applied early, so messages were reordered or landed at more than one offset, and some acknowledged messages never showed up. Several issues were fixed in 21.10.2 through 21.11.15, the duplicate-writes-by-default fix was slated for 22.1.1, and others stayed open (Jepsen).
- Jepsen, 2025: NATS JetStream loses acknowledged writes on power loss. JetStream acknowledges a publish immediately but by default flushes to disk only about every two minutes. In a simulated power failure Jepsen lost 131,418 of 930,005 acknowledged messages, about 30 seconds of writes, and corrupted data files caused much larger losses. NATS chose to document the risk rather than change the default (Jepsen).
Trade-offs
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| At most once | Simplest, lowest latency | Silent loss on crash | Telemetry, best-effort notifications |
| At least once | No loss, simple to run | Duplicates must be handled | Default for most pipelines |
| At least once + idempotent consumer | Exactly-once effect, works with any sink | Dedup storage and key design | Money, inventory, anything counted |
| Broker transactions (Kafka EOS) | Atomic read-process-write within Kafka | Only covers Kafka, adds latency | Stream processing between topics |
Pitfalls in production
- Broker-generated ids for dedup. A producer retry creates a new broker id, so dedup misses it. Use an id assigned by the producer or derived from the business event.
- Dedup check outside the transaction. Check-then-act races let two consumers both apply. Insert the id and apply the effect in one transaction, or use a unique constraint.
- Dedup window shorter than redelivery window. Old duplicates slip through after the id expires. Size retention from the maximum redelivery delay, including DLQ replays.
- Side effects that cannot be deduplicated. Emails and third-party API calls. Pass an idempotency key to the third party or record the send before acking.
- Believing "exactly once" marketing. It usually holds only inside one system. Trace each hop and write down its guarantee.
Leadership lens
- Make "what happens if this message is processed twice?" a standard design review question.
- Standardise event envelopes with a producer-assigned id, timestamp and schema version.
- Prefer at-least-once with idempotency over distributed transactions; it composes across systems.
- Budget the storage and latency of dedup tables explicitly.
Check yourself
Why is exactly-once delivery impossible but exactly-once processing possible?
Show answer
A sender cannot distinguish a lost message from a lost ack, so it must risk duplicates or loss. The receiver can, however, detect and discard duplicates so the effect happens once.
Why must the dedup record and the side effect share a transaction?
Show answer
Otherwise a crash between them either applies the effect without recording it, causing a later duplicate, or records it without applying, causing a loss.
Does Kafka's exactly-once mode protect a consumer that writes to an external database?
Show answer
No. It only makes Kafka reads and writes atomic together; the external write needs its own idempotency.
Sources and further study
Primary
- KIP-98: Exactly once delivery and transactional messaging: the design of producer ids, epochs, sequence numbers and transaction markers.
- KAFKA-13598: idempotent producer not enabled by default: the bug that kept Kafka 3.0's new default from taking effect, and the versions that fixed it.
- Jepsen: NATS 2.12.1 (2025): what an acknowledgement is worth when the broker flushes lazily.
Systems docs
- Apache Kafka producer configuration:
acks,enable.idempotenceand the in-flight limit, with the rules for when idempotence turns itself off. - Amazon SQS: exactly-once processing: FIFO deduplication ids and the 5-minute interval.
- Google Pub/Sub subscription API reference: what its exactly-once delivery flag does and does not cover.
Books
- Designing Data-Intensive Applications (2nd ed.), Martin Kleppmann and Chris Riccomini, Ch 12 "Stream Processing" and Ch 13 "A Philosophy of Streaming Systems": fault tolerance in stream processors and the "Aiming for Correctness" argument for end-to-end idempotence.
- Understanding Distributed Systems (2nd ed.), Roberto Vitillo, chapter "Asynchronous transactions": keeping services consistent over messaging when a distributed transaction is not an option.
Other curricula
- Azure Architecture Center: Idempotent Consumer pattern: treats at-least-once delivery as a given and puts deduplication in the consumer.
- donnemartin/system-design-primer: mentions only that SQS may deliver a message twice; it does not cover delivery semantics as a topic.
Watch
- Kafka Delivery Semantics (Irtiza Hafiz): at-most, at-least and exactly once in Kafka terms.
- Kafka - Exactly once semantics with Matthias J. Sax (The Geek Narrator): a Kafka committer on how EOS really works.
Free resources
- Kafka documentation: message delivery semantics: official explanation of each guarantee in Kafka.
- Exactly-once semantics in Apache Kafka (Confluent): idempotent producers and transactions.
- Transactional outbox (microservices.io): publishing events reliably from a database transaction.
- Making retries safe with idempotent APIs (AWS Builders' Library): idempotency design at AWS.
- Two Generals' Problem (Wikipedia): why the ambiguity cannot be removed.