{"schemaVersion":"1.0","type":"Article","types":["Article"],"slug":"kafka-delivery-guarantees-at-most-once-at-least-once-and-exactly-once-pe0u6","url":"https://zyvop.com/kafka-delivery-guarantees-at-most-once-at-least-once-and-exactly-once-pe0u6","title":"Kafka Delivery Guarantees: At-Most-Once, At-Least-Once, and Exactly-Once","subtitle":"At-most-once, at-least-once, and exactly-once explained, with the producer, consumer, and transaction settings that decide which one you get.","tldr":"Can Kafka lose a message, or deliver it twice? It depends on your configuration. This post compares the three delivery guarantees, shows how idempotent producers and transactions work, and covers where exactly-once stops.","keywords":["Delivery Guarantees","Reliability","Transactions","Exactly-Once","Idempotence","Tutorial","Apache Kafka from Zero to Production"],"entities":["Samod Alex","Delivery Guarantees","Reliability","Transactions","Exactly-Once","Idempotence","Tutorial","Apache Kafka from Zero to Production","ZyVOP"],"keyTakeaways":["Part 5 of Apache Kafka from Zero to Production.","It can do either, or neither, depending on how you configure your producers and consumers.","Kafka does not choose a delivery guarantee for you."],"headings":["The three guarantees","Where messages go wrong","At most once","At least once","Exactly once: idempotence plus transactions","Where exactly once stops","Choosing a guarantee","Settings at a glance","Mistakes to avoid","Next in the series","Frequently Asked Questions","What are the three Kafka delivery guarantees and how do they differ?","How does a producer achieve at‑least‑once delivery?","What consumer setting is required to read only committed records for exactly‑once processing?","How do Kafka transactions provide exactly‑once semantics?","Does Kafka's exactly‑once guarantee prevent duplicate effects outside the cluster, such as sending emails or writing to a database?","What are common ways to handle duplicate records when using at‑least‑once delivery?","What are typical mistakes that prevent achieving the intended delivery guarantee?"],"outboundLinks":[],"contentText":"Part 5 of Apache Kafka from Zero to Production. Can Kafka lose a message, or deliver it twice? It can do either, or neither, depending on how you configure your producers and consumers. Kafka does not choose a delivery guarantee for you. Parts 2 and 3 introduced the settings, and this post puts them together into the three guarantees you can build. The three guarantees Guarantee Producer side Consumer side What can go wrong At most once acks=0, or no retries Commit offsets before processing Records can be lost, but never repeated At least once acks=all with retries Commit offsets after processing Nothing is lost, but records can repeat Exactly once Idempotence plus transactions read_committed, with offsets committed in the transaction Each result appears once in Kafka, with limits covered below Each guarantee is the combination of choices on both sides of the log. A reliable producer feeding a consumer that commits too early still loses data. Where messages go wrong A record travels in two hops, producer to broker and broker to consumer, and a failure at either one forces a choice between losing the record and repeating it. The acknowledgement is lost. The producer sent a record and the broker stored it, but the reply never arrived. If the producer retries, the record is written twice. If it does not retry, a real failure would lose the record. The leader fails before replication. With acks=1, a record acknowledged by the leader alone can vanish when that leader dies. The consumer commits too early. It commits an offset, then crashes before finishing the work. On restart, the record is skipped. The consumer commits too late. It finishes the work, then crashes before committing. On restart, the record is processed again. At most once On the producer side, this means acks=0 or disabling retries. On the consumer side, it means committing the offset before processing the record. If anything fails in between, that record is gone. This fits data where an occasional gap does not matter and repeats would be worse, such as high-volume metrics or sampled telemetry. For most business data it is the wrong choice. At least once This is the practical default. Producers use acks=all with retries (both are on by default in current Kafka versions), and consumers commit only after processing, as in the loop from Part 3. Nothing acknowledged is lost, but a crash or rebalance can cause some records to be delivered again. The standard way to live with duplicates is to make processing idempotent, meaning that running it twice has the same effect as running it once. A few common techniques: Write with an upsert keyed on a unique ID carried in the event, so a repeated write overwrites itself. Keep a record of processed event IDs and skip any that appear again. Design operations so repeating them changes nothing, such as setting a status instead of incrementing a counter. At-least-once with idempotent handlers is what most Kafka applications should use. Exactly once: idempotence plus transactions Part 2 showed that the idempotent producer removes duplicates caused by retries, but only within one producer session. Transactions extend this to a whole unit of work. In a consume-transform-produce loop, a transaction makes three things happen together or not at all: the output records are written, possibly to several partitions and topics, and the input offsets are committed. It works through a few pieces: A transactional ID (transactional.id) identifies the producer across restarts. A broker-side transaction coordinator tracks its transactions in an internal topic. When a new instance starts with the same ID, the older one is fenced and can no longer write, which stops zombie instances from corrupting results. On commit or abort, the transaction coordinator writes a marker to each partition involved. Consumers set isolation.level=read_committed so they skip records from aborted transactions and wait for open ones to finish. By default, consumers read everything, including uncommitted records. Here is the core of a transactional loop in Java, assuming a consumer with auto-commit off and read_committed, and a producer created with a transactional.id: producer.initTransactions(); while (true) { ConsumerRecords&lt;String, String&gt; records = consumer.poll(Duration.ofMillis(500)); if (records.isEmpty()) continue; producer.beginTransaction(); try { Map&lt;TopicPartition, OffsetAndMetadata&gt; offsets = new HashMap&lt;&gt;(); for (ConsumerRecord&lt;String, String&gt; record : records) { producer.send(new ProducerRecord&lt;&gt;(\"orders-enriched\", record.key(), enrich(record.value()))); offsets.put(new TopicPartition(record.topic(), record.partition()), new OffsetAndMetadata(record.offset() + 1)); } producer.sendOffsetsToTransaction(offsets, consumer.groupMetadata()); producer.commitTransaction(); } catch (ProducerFencedException e) { producer.close(); // a newer instance with the same transactional.id took over throw e; } catch (KafkaException e) { producer.abortTransaction(); // outputs and offsets roll back together // rewind the consumer to its last committed offsets so the batch is read again } }The important line is sendOffsetsToTransaction. Because the consumer's progress is committed inside the same transaction as the output, a crash can never leave results written but offsets uncommitted, or the reverse. If you use Kafka Streams, you get all of this by setting processing.guarantee=exactly_once_v2, without writing the loop yourself. Transactions are not free. They add some latency and coordination work, and a long-running open transaction holds back read_committed consumers at the last stable offset, the position before the first unfinished transaction. Using one transaction per batch of records, rather than per record, keeps the overhead small. Where exactly once stops Kafka's exactly-once guarantee covers reading from Kafka and writing back to Kafka. It cannot reach outside the cluster. If your processing sends an email, calls an API, or writes to a database, Kafka cannot undo or deduplicate those effects when a transaction aborts and the batch runs again. For those cases, combine at-least-once delivery with one of these approaches: Make the external write idempotent, using a unique event ID as the key. Store the consumer's offsets in the same database transaction as the results, and resume from the database's offsets on restart. Use an outbox table to publish events from a database change reliably. When people say Kafka offers exactly-once processing, what they mean is exactly-once effects on Kafka topics. Past the edge of the cluster, idempotence is your tool. Choosing a guarantee Metrics, logs, and telemetry where gaps are tolerable: at most once, or at least once if the cost is low. Most business events: at least once with idempotent handlers. Pipelines from Kafka to Kafka with correctness requirements, such as financial calculations or aggregations: transactions or Kafka Streams with exactly-once. Settings at a glance Setting What it controls Default acks How many replicas confirm a write all enable.idempotence Prevents duplicates and reordering from producer retries true transactional.id Identifies a producer so it can use transactions None (set it to enable transactions) transaction.timeout.ms How long a transaction may stay open before it is aborted 1 minute isolation.level (consumer) Whether consumers see uncommitted transactional records read_uncommitted enable.auto.commit (consumer) Commit offsets automatically on a timer true processing.guarantee (Kafka Streams) Streams delivery guarantee at_least_once Mistakes to avoid Assuming the defaults give exactly-once. They give at-least-once on the producer side, and consumers still need commit and processing logic that handles repeats. Forgetting read_committed on consumers. Without it, they read aborted and in-flight records and see duplicates. Sharing one transactional.id across running instances. They fence each other. Give every producer instance its own stable ID. Expecting exactly-once to cover databases and APIs. It stops at the Kafka boundary. Keeping transactions open for a long time. They delay every read_committed consumer behind them. Next in the series This is Part 5 of Apache Kafka from Zero to Production. In Part 6, we look at Kafka Connect and Kafka Streams: moving data in and out of Kafka without custom code, and processing it as it flows. Frequently Asked Questions What are the three Kafka delivery guarantees and how do they differ? At‑most‑once may lose records but never repeats (producer acks=0 or no retries, consumer commits offsets before processing). At‑least‑once never loses records but may repeat them (producer acks=all with retries, consumer commits after processing). Exactly‑once ensures each record appears once in Kafka by using idempotent producers and transactions together with read_committed consumers. How does a producer achieve at‑least‑once delivery? By setting acks=all and enabling retries (both on by default), the producer only considers a record written when all in‑sync replicas acknowledge it, and it retries on failure, guaranteeing no loss. What consumer setting is required to read only committed records for exactly‑once processing? Set isolation.level=read_committed so the consumer skips records from aborted transactions and sees only fully committed data. How do Kafka transactions provide exactly‑once semantics? A transaction groups output writes and offset commits into a single atomic unit; on commit, both the produced records and the consumer offsets become visible together, preventing partial results or duplicate processing. Does Kafka's exactly‑once guarantee prevent duplicate effects outside the cluster, such as sending emails or writing to a database? No; exactly‑once only covers Kafka topics. External side effects must be made idempotent or handled with other techniques because Kafka cannot roll them back. What are common ways to handle duplicate records when using at‑least‑once delivery? Make processing idempotent, for example by upserting with a unique key, tracking processed event IDs to skip repeats, or designing operations that are safe to repeat (e.g., setting a status instead of incrementing a counter). What are typical mistakes that prevent achieving the intended delivery guarantee? Assuming defaults give exactly‑once, forgetting to set read_committed, sharing a transactional.id across instances (causing fencing), expecting exactly‑once to cover external systems, and keeping transactions open too long, which delays read_committed consumers.","contentHash":"sha256:2d52466cb8dd68a440b0fb0728a48a41d4207076c2549483a446baa6ad8bc05f","authorName":"Samod Alex","authorUrl":"https://zyvop.com/author/samod","authorSameAs":[],"category":"Tutorial","tags":["Delivery Guarantees","Reliability","Transactions","Exactly-Once","Idempotence"],"audience":"Software engineers and developers building applications with Tutorial","tone":"Practical and evidence-based engineering guidance","readingTimeMinutes":7,"wordCount":1616,"faqs":[{"question":"What are the three Kafka delivery guarantees and how do they differ?","answer":"At‑most‑once may lose records but never repeats (producer acks=0 or no retries, consumer commits offsets before processing). At‑least‑once never loses records but may repeat them (producer acks=all with retries, consumer commits after processing). Exactly‑once ensures each record appears once in Kafka by using idempotent producers and transactions together with read_committed consumers."},{"question":"How does a producer achieve at‑least‑once delivery?","answer":"By setting acks=all and enabling retries (both on by default), the producer only considers a record written when all in‑sync replicas acknowledge it, and it retries on failure, guaranteeing no loss."},{"question":"What consumer setting is required to read only committed records for exactly‑once processing?","answer":"Set isolation.level=read_committed so the consumer skips records from aborted transactions and sees only fully committed data."},{"question":"How do Kafka transactions provide exactly‑once semantics?","answer":"A transaction groups output writes and offset commits into a single atomic unit; on commit, both the produced records and the consumer offsets become visible together, preventing partial results or duplicate processing."},{"question":"Does Kafka's exactly‑once guarantee prevent duplicate effects outside the cluster, such as sending emails or writing to a database?","answer":"No; exactly‑once only covers Kafka topics. External side effects must be made idempotent or handled with other techniques because Kafka cannot roll them back."},{"question":"What are common ways to handle duplicate records when using at‑least‑once delivery?","answer":"Make processing idempotent, for example by upserting with a unique key, tracking processed event IDs to skip repeats, or designing operations that are safe to repeat (e.g., setting a status instead of incrementing a counter)."},{"question":"What are typical mistakes that prevent achieving the intended delivery guarantee?","answer":"Assuming defaults give exactly‑once, forgetting to set read_committed, sharing a transactional.id across instances (causing fencing), expecting exactly‑once to cover external systems, and keeping transactions open too long, which delays read_committed consumers."}],"primaryTopic":"Tutorial","publishedAt":"2026-10-15T07:32:00.029Z","updatedAt":"2026-10-17T06:04:30.963Z","canonicalUrl":"https://zyvop.com/kafka-delivery-guarantees-at-most-once-at-least-once-and-exactly-once-pe0u6"}