Achieving Exactly-Once Delivery in Apache Kafka with Idempotent Producers and Transactions
Enable Kafka exactly-once by turning on idempotent producers and using the transactional API. This guide shows the mechanism, a complete Java example, limits, and verification commands.
03 May 2026, 18:54 UTC

Problem and Takeaway
Duplicate records appear when a producer retries after a network glitch or a broker fails mid‑write. Kafka solves this by combining idempotent producers (automatic deduplication per partition) with the transactional API (atomic multi‑partition writes). Enable both and you get exactly‑once semantics without application‑level deduplication.
How It Works
Producer ID and Sequence Numbers
When enable.idempotence=true (default since Kafka 3.0) the broker assigns each producer a unique PID. Every record sent to a partition carries a monotonically increasing sequence number. The broker stores the highest committed sequence number per PID in the partition log and discards any later record with a duplicate or lower sequence number.
Transactional Boundaries
A transactional.id ties a logical producer instance to its PID across restarts. The broker keeps the PID‑to‑sequence mapping for transactional.id.expiration.ms (default 7 days). Calls to initTransactions(), beginTransaction(), send(), and commitTransaction() write transaction markers (commit/abort) into the log. Consumers with isolation.level=read_committed only advance past committed markers.
Worked Java Configuration
Properties props = new Properties();
props.put("bootstrap.servers", "broker1:9092,broker2:9092");
props.put("enable.idempotence", true); // default in 3.0+
props.put("transactional.id", "txn-" + appInstanceId); // unique per logical producer
props.put("acks", "all"); // enforced by idempotence
props.put("max.in.flight.requests.per.connection", 5); // enforced by idempotence
KafkaProducer producer = new KafkaProducer<>(props);
producer.initTransactions();
try {
producer.beginTransaction();
producer.send(new ProducerRecord<>("orders", key, value));
producer.send(new ProducerRecord<>("inventory", key, value));
producer.commitTransaction();
} catch (Exception e) {
producer.abortTransaction();
throw e;
} finally {
producer.close();
}Each logical producer instance must use a distinct transactional.id. Reusing the same ID concurrently raises ProducerFencedException.
Limits and Common Mistakes
- Cross‑cluster exactly‑once is not provided; MirrorMaker 2 runs independent transactions per cluster.
- Downstream duplicates remain possible — consumers or external systems still need idempotent processing or deduplication.
- Transaction timeout (
transaction.timeout.ms, default 1 min) must exceed the longest processing window; too short aborts valid work, too long delays broker cleanup. - Consumer latency rises with
read_committedbecause the consumer waits for commit markers. - PID expiration: a producer idle longer than
transactional.id.expiration.msloses its PID and may emit duplicates on restart. - Ordering across partitions is not guaranteed; use a single partition or keyed messages for per‑key order.
Verification Steps
- Unit test: send 10 000 records in a transaction, kill the producer mid‑transaction, restart with the same
transactional.id, and consume withisolation.level=read_committed. Expect zero duplicates. - Broker log check: grep for
Completed transactionandAborted transactionmarkers; confirmtransactional.id.expiration.msmatches retention policy. - Metrics: monitor JMX
producer-metrics:record-send-rate,request-latency-avg,transaction-commit-rateto quantify overhead (typically 3‑10 % throughput loss). - Consumer offset inspection: use
kafka-consumer-groups.sh --describe --group my-groupand verify fetched offsets never pass uncommitted transaction markers. - Performance benchmark: run
kafka-producer-perf-test --topic test --num-records 1000000 --record-size 1000 --throughput -1 --producer.config producer.properties --transactional-id bench-txnwith and withoutenable.idempotenceto measure the difference.
Quick Comparison
| Mode | Throughput (relative) | Latency Impact | Guarantees |
|---|---|---|---|
| Non‑idempotent, no transactions | 100 % | Baseline | At‑least‑once |
| Idempotent only | ≈ 95‑97 % | Minimal | Exactly‑once per partition |
| Idempotent + transactions | ≈ 90‑97 % | + transaction marker wait | Exactly‑once across partitions |
Tune batch.size and linger.ms as usual; they still apply on top of the idempotence overhead.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.