Choosing Kafka Delivery Semantics: Idempotent vs. Transactional Exactly-Once
Decide between idempotent and transactional delivery in Kafka. Compare latency, operational complexity, and configurations for financial event processing.
23 Jul 2025, 07:04 UTC

The Problem: Balancing Data Integrity and Latency
When processing financial events, duplicate records can lead to incorrect balances or double-billing, while data loss is unacceptable. However, achieving \"exactly-once\" processing often introduces latency that can push p99 response times beyond acceptable limits (e.g., >50ms). The core decision is whether to use Idempotent Producers, which prevent duplicates from network retries, or Transactional API, which ensures atomic writes across multiple partitions.
The useful takeaway: Use enable.idempotence=true for high-throughput, single-partition integrity with minimal overhead. Use a transactional.id only when you require atomic writes across multiple topics or partitions and can tolerate the latency of the Transaction Coordinator.
Comparing Delivery Semantics
| Semantics | Producer Config | Consumer Config | Duplicate Risk | Multi-Partition Atomicity | Latency Impact |
|---|---|---|---|---|---|
| At-most-once | enable.idempotence=false, acks=1 | Default | High (Loss/Dupes) | No | Lowest |
| At-least-once (Idempotent) | enable.idempotence=true, acks=all | Default | Low (Session-based) | No | Low |
| Exactly-once (Transactional) | transactional.id set, acks=all | isolation.level=read_committed | Prevented | Yes | Higher |
Trade-offs and Operational Constraints
Idempotent Producers eliminate duplicates caused by producer retries within a single session. Kafka achieves this by assigning a producer ID (PID) and a sequence number to every batch. If the broker receives a sequence number it has already committed, it rejects the duplicate. This is a low-overhead operation that does not require a global coordinator.
Transactional Semantics provide a stronger guarantee: either all messages in a transaction are visible to the consumer, or none are. This requires a Transaction Coordinator (a broker-side module) and a transaction state log. The trade-offs include:
- Latency: Every transaction involves multiple round trips to the coordinator to mark the transaction as 'Ongoing', 'PrepareCommit', and 'Committed'.
- Consumer Lag: Consumers using
read_committedcannot read past the first open transaction, meaning a hung producer can stall the entire consumer group's progress. - Operational Load: The
transaction.state.logmust be properly replicated (typicallyreplication.factor=3) to avoid becoming a single point of failure.
Implementation: Transactional Exactly-Once
To implement transactional writes (supported in Kafka 0.11+), you must configure both the producer and consumer. Run these configurations on your application hosts with appropriate ACLs for the target topics.
Producer Configuration
# Required for transactions
enable.idempotence=true
acks=all
retries=2147483647
# Must be unique per producer instance to prevent fencing
transactional.id=tx-financial-service-01
Producer Logic Pattern
// Initialize transactions once at startup
producer.initTransactions();
try {
producer.beginTransaction();
producer.send(new ProducerRecord<String, String>(\"account-updates\", key1, val1));
producer.send(new ProducerRecord<String, String>(\"audit-log\", key2, val2));
producer.commitTransaction();
} catch (ProducerFencedException | OutOfOrderSequenceException e) {
// Fatal errors: producer must be closed
producer.close();
} catch (KafkaException e) {
producer.abortTransaction();
}Consumer Configuration
# Only read messages from committed transactions
isolation.level=read_committed
# Manual offset management is required for end-to-end exactly-once
enable.auto.commit=false
Validation and Verification
To verify the implementation, perform the following checks on your broker and application:
- Broker Check: Verify
transaction.state.log.enabled=trueis set in the server properties. - Failure Test: Produce a deterministic payload, force a broker restart mid-batch, and verify that the consumer with
read_committedsees the record exactly once. - Isolation Test: Start a transaction but do not commit it. Verify that the consumer does not see these records until
commitTransaction()is called. - Metric Monitoring: Monitor the
transaction-abort-rateon the producer. A high rate indicates contention or timeouts, which will spike your p99 latency.
Limitations
Kafka's exactly-once is broker-level. If your consumer processes a message and then crashes before committing the offset to Kafka, the message will be redelivered. To achieve end-to-end exactly-once, your downstream application must be idempotent (e.g., using a unique transaction ID in a database) or use the Kafka-to-Kafka transactional read-process-write pattern.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.