Skip to content

Kafka Exactly-Once Semantics: Delivery Guarantees, Idempotence, and Transactions

2025-06-05 · Updated 2026-07-18 · 12 min read · Igor Bobriakov

Achieving Exactly-Once Semantics (EOS) in Apache Kafka: A Practical Implementation Guide

Kafka exactly-once semantics only make sense when you connect them to the wider delivery-guarantee model: at-most-once, at-least-once, and exactly-once. From there, the real implementation levers are idempotent producers, transactions, and careful consumer behavior.

The first boundary matters: idempotence prevents duplicate writes caused by a producer retry. Transactions can make Kafka output records and consumed offsets atomic in a read-process-write path. Neither mechanism automatically makes a database write, API call, or other external side effect exactly once.

Understanding Message Delivery Guarantees

Before diving into EOS, let’s quickly recap the common message delivery guarantees:

  • At-Most-Once: Messages may be lost but are never redelivered. This is the “fire and forget” approach, suitable for non-critical data where loss is acceptable.
  • At-Least-Once: Messages are never lost but may be redelivered. This is Kafka’s default guarantee for producers if retries are enabled and acks is not 0. Applications must be designed to handle potential duplicates, for example through idempotent consumers or deduplication logic.
  • Exactly-Once: Messages are delivered and processed precisely once. This is the most desirable but also the most complex to achieve.
SemanticMessage LossMessage DuplicationTypical Use Case
At-Most-OncePossibleNoMetrics, logging where occasional loss is tolerable
At-Least-OnceNoPossibleMost general use cases, with downstream deduplication
Exactly-OnceNoNoFinancial transactions, critical state updates, billing systems

How Apache Kafka Achieves Exactly-Once Semantics

Kafka achieves EOS through a combination of two key features: idempotent producers and transactions, built upon a foundation of reliable replication and leader election.

1. Idempotent Producer

An idempotent operation is one that can be performed multiple times with the same effect as if it were performed only once. In Kafka, the idempotent producer ensures that producer retries, due to transient network issues for example, do not result in duplicate messages being written to a topic partition.

How it works: When idempotence is enabled (enable.idempotence=true), the producer is assigned a unique Producer ID (PID) and maintains a sequence number for each message sent to a specific topic-partition. The broker keeps track of the highest sequence number successfully written for each PID. If the producer retries sending a message with a sequence number less than or equal to the one already acknowledged by the broker for that PID and partition, the broker discards the duplicate message but still sends an acknowledgment to the producer.

Idempotent Producer Flow

Diagram 1: Idempotent Producer Flow. The broker uses PID and sequence numbers to detect and discard duplicates from producer retries.

Key configurations for an idempotent producer:

  • enable.idempotence=true: Enables idempotence.
  • This implicitly sets:
    • acks=all: Ensures the leader waits for all in-sync replicas to acknowledge the write.
    • retries > 0: For example, Integer.MAX_VALUE for robust retrying.
    • max.in.flight.requests.per.connection=5 or less. Kafka can maintain ordering with idempotence enabled.

2. Kafka Transactions (Atomic Read-Process-Write)

While idempotence solves producer-side duplicates, EOS often involves consuming messages, processing them, and producing new messages atomically in a read-process-write pattern. Kafka transactions allow grouping multiple produce operations and consumer offset commits into a single atomic unit.

How it works:

  1. Initialization: The producer is configured with a unique transactional.id. This ID allows the producer to maintain its transactional state across application restarts. The producer calls initTransactions().
  2. Begin Transaction: The producer calls beginTransaction().
  3. Consume-Process-Produce:
    • The application consumes messages.
    • It processes them.
    • It produces new messages using the transactional producer via producer.send(). These messages are staged and not visible to consumers with isolation.level=read_committed until the transaction is committed.
    • It sends consumer offsets to the transaction coordinator using producer.sendOffsetsToTransaction(). This links the consumed offsets with the current transaction.
  4. Commit/Abort Transaction:
    • If processing is successful, the producer calls commitTransaction(). The transaction coordinator then orchestrates a two-phase commit protocol to make all produced messages and committed offsets visible atomically.
    • If an error occurs, the producer calls abortTransaction(). All staged messages are discarded and offsets are not committed.

A central component called the Transaction Coordinator, running on a Kafka broker, manages the state of transactions.

Kafka Transactional Producer Flow with Transaction Coordinator

Diagram 2: Kafka Transactional Producer Flow with Transaction Coordinator.

Consumer Configuration for EOS

For consumers to only read committed transactional data, they must be configured with:

  • isolation.level=read_committed: This ensures the consumer only reads messages that are part of a successfully committed transaction. It will not read messages from ongoing or aborted transactions. The default is read_uncommitted. A read_committed consumer can read only up to the last stable offset while a transaction remains open, so long transactions can delay visibility. See the official consumer configuration reference.

Practical Implementation Guide

Enabling Idempotence (Producers)

This is the simplest step toward EOS and protects against producer-retry duplicates.

// Java Producer Configuration for Idempotence
Properties props = new Properties();
props.put("bootstrap.servers", "kafka-broker1:9092,kafka-broker2:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// Enable idempotence
props.put("enable.idempotence", "true");
// The following are typically set by enable.idempotence=true, but good to be aware of:
// props.put("acks", "all"); // Ensures durability
// props.put("retries", String.valueOf(Integer.MAX_VALUE)); // Retry indefinitely or a large number
// props.put("max.in.flight.requests.per.connection", "5"); // Kafka handles ordering correctly with idempotence
Producer<String, String> producer = new KafkaProducer<>(props);
try {
producer.send(new ProducerRecord<>("my-topic", "key", "message-value-1")).get();
producer.send(new ProducerRecord<>("my-topic", "key", "message-value-2")).get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
producer.close();
}

Implementing Transactions (Producers and Consumers)

This provides atomicity for read-process-write patterns.

// Java Transactional Producer Example
Properties producerProps = new Properties();
producerProps.put("bootstrap.servers", "kafka-broker1:9092");
producerProps.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
producerProps.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
producerProps.put("enable.idempotence", "true");
producerProps.put("transactional.id", "my-unique-transactional-id");
Producer<String, String> producer = new KafkaProducer<>(producerProps);
producer.initTransactions();
try {
producer.beginTransaction();
// Simulate consuming messages (in reality, this would come from a KafkaConsumer)
// Map<TopicPartition, OffsetAndMetadata> offsetsToCommit = ... ;
producer.send(new ProducerRecord<>("output-topic-1", "key1", "processed-value-1"));
producer.send(new ProducerRecord<>("output-topic-2", "key2", "processed-value-2"));
// producer.sendOffsetsToTransaction(offsetsToCommit, "my-consumer-group-id");
producer.commitTransaction();
System.out.println("Transaction committed successfully.");
} catch (ProducerFencedException | OutOfOrderSequenceException | AuthorizationException e) {
System.err.println("Fatal error during transaction: " + e.getMessage());
producer.close();
throw e;
} catch (KafkaException e) {
System.err.println("Error during transaction, aborting: " + e.getMessage());
try {
producer.abortTransaction();
} catch (KafkaException abortException) {
System.err.println("Error aborting transaction: " + abortException.getMessage());
}
} finally {
producer.close();
}
// Java Consumer Configuration for EOS
Properties consumerProps = new Properties();
consumerProps.put("bootstrap.servers", "kafka-broker1:9092");
consumerProps.put("group.id", "my-eos-consumer-group");
consumerProps.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
consumerProps.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
consumerProps.put("enable.auto.commit", "false");
consumerProps.put("isolation.level", "read_committed");

Expert Insight: Transactional ID Uniqueness

The `transactional.id` must be unique across all running producer instances. If multiple producer instances use the same `transactional.id`, older instances will be fenced off by the broker with a `ProducerFencedException` to prevent zombie instances from interfering with the active one. Careful management of `transactional.id` is critical, especially in dynamically scaled environments.

Kafka Streams and EOS

Apache Kafka Streams significantly simplifies implementing EOS for stream processing applications. By default, Kafka Streams provides at-least-once processing guarantees. To enable EOS, you can set:

  • processing.guarantee="exactly_once_v2" on supported current Kafka releases.

The older exactly_once configuration names are deprecated in current Kafka. With exactly_once_v2, Kafka Streams manages producer idempotence, transactions for read-process-write-update-state operations, and consumer offset commits atomically. Confirm the broker/client compatibility requirements in the current Kafka Streams configuration reference.

Conceptual Read-Process-Write-Update State Cycle in Kafka Streams with EOS

Diagram 3: Conceptual Read-Process-Write-Update State Cycle in Kafka Streams with EOS.

Key Configurations for EOS

Here’s a quick reference for the most important configurations related to EOS:

ScopeParameterEOS Recommended ValueDescription
Producerenable.idempotencetrueEnables idempotent message delivery.
ProduceracksallLeader waits for all ISRs to acknowledge. Set automatically by enable.idempotence=true.
ProducerretriesInteger.MAX_VALUE or another large numberEnables retries on transient errors.
Producertransactional.idUnique string per logical producerEnables transactional capabilities for the producer. Must be unique.
Producermax.in.flight.requests.per.connection1 to 5With enable.idempotence=true, Kafka guarantees ordering even with this up to 5.
Consumerisolation.levelread_committedEnsures the consumer only reads messages from committed transactions.
Consumerenable.auto.commitfalseRequired when managing offsets as part of a transaction or manually.
Brokertransaction.state.log.replication.factor>= 3 for productionReplication factor for the internal transaction log topic.
Brokertransaction.state.log.min.isr2 when replication factor is 3Minimum ISR count for the transaction log topic.
Kafka Streamsprocessing.guaranteeexactly_once_v2Enables EOS for Kafka Streams applications.

Best Practices and Considerations for EOS

  • Thoroughly Test Your Application: EOS behavior, especially under failure scenarios like broker restarts, network partitions, and consumer crashes, needs rigorous testing.
  • Manage transactional.id Carefully: Ensure uniqueness and proper lifecycle management, especially in dynamic or containerized environments.
  • Keep Transactions Short: Long-running transactions can increase load on the transaction coordinator, increase the risk of timeouts, and delay visibility for read_committed consumers.
  • Monitor Transactional Applications: Pay close attention to Kafka metrics related to transactions, such as transaction coordinator load, transaction rates, abort rates, and fencing counts.
  • Understand Performance Implications: EOS introduces overhead compared to at-least-once delivery. Benchmark your workload to understand the impact on throughput and latency.
  • Handle ProducerFencedException Gracefully: This indicates another producer instance with the same transactional.id has started. The current instance should shut down cleanly.
  • Error Handling is Key: Implement robust error handling for Kafka exceptions. Decide when to abort a transaction and retry, and when an error is fatal for the producer instance.

Expert Insight: When is EOS Truly Necessary? While EOS is powerful, it’s not always required. If your downstream systems can handle duplicates idempotently, for example by using unique keys in a database UPSERT operation, at-least-once delivery from Kafka combined with downstream deduplication might be a simpler and more performant solution.

Common Pitfalls and How to Avoid Them

  • Mismatched Producer and Consumer Configurations: Forgetting isolation.level=read_committed on consumers when producers are transactional is a common mistake, leading to consumers seeing uncommitted data.
  • Incorrect transactional.id Management: Reusing transactional.id values inappropriately can lead to fencing, especially in dynamic environments.
  • Transaction Timeouts: If processing within a transaction takes longer than the configured timeout, the broker may abort the transaction or the consumer may be removed from the group.
  • Not Handling Fencing or Sequence Exceptions: Ignoring ProducerFencedException or OutOfOrderSequenceException can lead to unstable producer behavior.
  • Underestimating Operational Overhead: Transactional systems require more careful monitoring and deeper understanding of broker-side coordinator behavior.

Beyond Kafka: End-to-End Exactly-Once

Kafka’s EOS applies to the Kafka transaction boundary. If your application interacts with a database, API, or another queue, that external side effect is not made atomic merely by setting transactional.id. The official producer configuration also makes the narrower distinction explicit: transactional.id enables transactional delivery, while a missing ID leaves the producer idempotent only.

For an external sink, choose a boundary that can survive replay:

  • use an idempotency key or deterministic upsert in the destination;
  • write an outbox record in the same database transaction as the business state, then publish it through change data capture;
  • use a connector whose delivery and replay behavior you have tested;
  • reserve a distributed transaction for the rare case where every participant supports it and the operating cost is justified.

The acceptance test is a forced crash at each boundary: before the external write, after the write but before the offset/transaction commit, and after the commit. If replay changes the final business state, the path is not end-to-end exactly once.

Glossary of Terms

  • PID (Producer ID): A unique identifier assigned to a Kafka producer when idempotence is enabled. Used with sequence numbers to prevent duplicate messages from producer retries.
  • Sequence Number: A per-partition, monotonically increasing number assigned by an idempotent producer to each message. Helps brokers detect and discard duplicates.
  • Transactional ID (transactional.id): A unique identifier configured on a Kafka producer to enable transactional capabilities and maintain state across restarts.
  • Transaction Coordinator: A Kafka broker component responsible for managing the lifecycle of transactions.
  • Isolation Level (isolation.level): A Kafka consumer configuration (read_committed or read_uncommitted) that determines whether a consumer can read messages that are part of uncommitted transactions.
  • ProducerFencedException: An exception thrown by a transactional producer when another producer instance with the same transactional.id has been initialized.

Conclusion

Apache Kafka provides powerful mechanisms - idempotent producers and atomic transactions - to achieve Exactly-Once Semantics. While implementing EOS requires careful configuration, robust error handling, and an understanding of the underlying principles, it is an attainable goal for applications demanding the highest level of data integrity.

Use these features when the Kafka-side atomicity is required, and use destination-side idempotency or an outbox when the workflow crosses Kafka’s transaction boundary. The distinction is more important than the label “exactly once.”

Technical Review

Bring the system under review

Send the system context, constraints, and pressure. A Principal Engineer reviews it and recommends the next step.

[ SUBMIT SPECS ]

No SDRs. A Principal Engineer reviews every submission.

About the author

Igor Bobriakov

AI Architect. Author of Production-Ready AI Agents. 15 years deploying production AI platforms and agentic systems for enterprise clients and deep-tech startups.