Demystifying Kafka's Exactly-Once Semantics: Idempotence, Transactions, and Fencing

Arpit Bhayani

Arpit Bhayani

Sep 04, 2026 • 8 min read

Play

Note: This article is an AI-generated write-up based on the captions and transcript of the video above. Watch the embedded video for the full visual walk-through and nuances.

Demystifying Kafka’s Exactly-Once Semantics: Idempotence, Transactions, and Fencing

Kafka is a cornerstone of modern data architectures, providing robust, high-throughput, and fault-tolerant messaging. While it famously offers “at-least-once” delivery by default, achieving “exactly-once” semantics is a critical requirement for many applications, especially those involving financial transactions or critical state changes. This article delves into how Kafka meticulously implements exactly-once delivery guarantees across three distinct layers, debunking common myths and providing a solid intuition for solving complex distributed system problems.

Understanding At-Least-Once Delivery (The Default)

By default, Kafka guarantees “at-least-once” message delivery. This means that a message sent by a producer is guaranteed to be persisted in Kafka at least once.

Consider the following scenario:

  1. A producer sends a message to a Kafka broker.
  2. The broker successfully persists the message and sends an acknowledgment (ACK) back to the producer.
  3. Due to a network issue, the ACK is lost before reaching the producer.
  4. The producer, not having received an ACK, assumes the message was not delivered and retries sending it.
  5. The broker receives and persists the message again, resulting in a duplicate.

This “at-least-once” behavior is a common challenge in distributed systems, often referred to as a variation of the Two Generals’ Problem. While acceptable for some use cases, duplicates can lead to incorrect application state in others.

The Nuance of Exactly-Once Semantics in Kafka

It’s crucial to understand that Kafka’s exactly-once guarantee applies to delivery and storage, not processing. Kafka ensures that a message is delivered to the broker and persisted in the log exactly once. It does not guarantee that a consumer will process a message exactly once, as consumer-side processing logic is outside Kafka’s control. The consumer is responsible for implementing its own idempotency or transactional logic if exactly-once processing is required.

Kafka achieves exactly-once delivery through a layered approach:

Layer 1: Producer Idempotence

The first layer addresses the problem of duplicate messages arising from producer retries. Idempotence means that performing an operation multiple times has the same effect as performing it once. Kafka makes producers idempotent by default when enable.idempotence is set to true.

Here’s how it works:

Producer ID (PID)

When a producer connects to a Kafka broker, the broker assigns it a unique Producer ID (PID), which is an int64 value. This PID identifies a specific producer instance.

Sequence Numbers

For every batch of messages sent to a specific partition, the producer attaches an incrementing, per-partition sequence number. A batch can contain multiple messages.

Broker-Side Deduplication Logic

Each Kafka broker maintains a cache of the last accepted sequence numbers for the past five batches from each active PID on its partitions. When a broker receives a message batch:

  • If the sequence number is in order (e.g., last accepted was 7, new is 8): The broker accepts the batch, appends it to the partition log, and updates the last accepted sequence number.
  • If the sequence number is out of order (e.g., last accepted was 7, new is 10): The broker ignores the batch, assuming it’s a delayed or reordered retry that will be handled by a later, in-order batch.
  • If the sequence number is a duplicate (e.g., last accepted was 7, new is 7): The broker ignores the batch, as it has already successfully persisted this message.

This mechanism effectively deduplicates messages from a single producer instance retrying the same batch. Kafka does not inspect the message content for deduplication; it relies solely on the PID and sequence number. This is because two identical messages can be legitimate, distinct events.

Message Structure

A Kafka message batch internally includes:

  • Producer ID (PID)
  • Producer Epoch (discussed in Layer 2)
  • Base Sequence Number (the sequence number for the first message in the batch)
  • Records (the actual messages)

Layer 2: Transactional IDs and Epochs (Zombie Fencing)

Producer idempotence solves retries from a single, continuously active producer. However, distributed systems face the “zombie instance” problem:

  • A producer instance starts writing messages.
  • It experiences a prolonged network partition, a garbage collection pause, or crashes.
  • A new producer instance (or a different thread/server) takes over, gets a new PID, and starts writing the same logical data.
  • The original “zombie” producer instance recovers and resumes writing its delayed messages.

In this scenario, the broker sees two different PIDs and treats them as independent producers, happily appending duplicates from both. This is where transactional IDs and epochs come into play.

Introducing Transactional IDs

To protect against zombie instances, Kafka allows producers to specify a transactional.id. This is a user-defined, logical identifier for a group of producer instances that are logically performing the same task (e.g., “payment-processor-producer”).

When a producer with a transactional.id starts, it first registers with a special Transaction Coordinator (a Kafka broker).

Producer Epochs and Fencing

  1. When a producer with a transactional.id initiates a transaction (via initTransactions()), the Transaction Coordinator assigns it a Producer Epoch. This epoch is an incrementing counter associated with the transactional.id.
  2. If a new producer instance with the same transactional.id comes online and tries to initTransactions(), the coordinator will assign it a higher epoch number.
  3. Any message batch sent by a producer includes its Producer Epoch along with its PID and sequence number.
  4. When a broker receives a message batch, it checks the Producer Epoch. If the epoch in the message is lower than the currently active epoch for that transactional.id, the broker rejects the message batch. This is known as fencing.
  5. The “fenced” producer instance receives an exception (e.g., TransactionFencedException) and is expected to shut down, preventing it from writing stale or duplicate data.

This mechanism ensures that only the producer instance with the highest (most recent) epoch for a given transactional.id can successfully write messages, effectively “fencing out” zombie instances.

Layer 3: Distributed Transactions with 2-Phase Commit

The first two layers ensure exactly-once delivery for individual message batches. However, many applications require atomic writes across multiple partitions, potentially spanning different Kafka brokers. For example, a single logical operation might involve writing messages to an “orders” topic and an “inventory-updates” topic. This is where Kafka’s full-fledged transaction capabilities, built on a variation of the Two-Phase Commit (2PC) protocol, are essential.

Atomic Writes Across Partitions

Kafka transactions allow a producer to send a series of messages to multiple partitions (even across different topics and brokers) as a single atomic unit. Either all messages in the transaction are committed and become visible to consumers, or none are.

Transaction Coordinator

Similar to how the leader of a partition coordinates writes, a dedicated Transaction Coordinator manages the lifecycle of distributed transactions.

Transaction State Topic

Kafka uses an internal, inaccessible topic called __transaction_state (with 50 partitions by default) to store the state of ongoing transactions. The leader of the __transaction_state partition determined by hashing the transactional.id (e.g., hash(transactional.id) % 50) becomes the Transaction Coordinator for that specific transactional.id.

The 2-Phase Commit Flow

When a producer initiates and commits a transaction:

  1. Phase 1: Prepare Commit

    • The producer sends a Prepare Commit request to its Transaction Coordinator.
    • The coordinator records an immutable Prepare Commit decision in its log (on the __transaction_state topic). This decision is durable and indicates the transaction’s intent to commit. Unlike classic 2PC, Kafka’s append-only nature means no explicit resource locking or “ready to commit” votes are needed from individual partitions at this stage. The decision is simply logged.
    • The coordinator acknowledges the producer.
  2. Phase 2: Actual Commit

    • Upon successful preparation, the coordinator sends Write Transaction Markers (either COMMIT or ABORT) to the leader of every partition involved in the transaction. These markers are special control records appended to the respective partition logs.
    • These markers make the transaction’s messages visible (for COMMIT) or invisible (for ABORT) to consumers configured with isolation.level=read_committed.
    • Once all partition leaders acknowledge the marker, the coordinator records a Complete Commit decision in its log.

Fault Tolerance during 2PC: If the coordinator or any partition leader fails during the 2PC process, the transaction state is recovered from the __transaction_state topic. The coordinator will retry the transaction until it is fully committed or aborted, ensuring atomicity and durability.

Key Takeaway: Delivery vs. Processing

It’s a common misconception that Kafka’s exactly-once semantics guarantees exactly-once processing. As highlighted, Kafka provides exactly-once delivery to its logs. Consumers still need to be designed with idempotency in mind if exactly-once processing is a strict requirement for the application logic. This often involves storing offsets and processing results transactionally in the consumer’s downstream system.

Conclusion

Kafka’s journey to exactly-once semantics is a testament to sophisticated distributed systems design. By combining producer idempotence for single-instance retries, transactional IDs and epochs for zombie fencing, and a robust two-phase commit protocol for atomic distributed transactions, Kafka provides a powerful and reliable foundation for critical data pipelines. Understanding these layers is key to building resilient applications on top of Kafka.

Arpit Bhayani

Principal Engineer II at Razorpay - building Agent Studio, Ex-staff engg at GCP Memorystore & Dataproc, Creator of DiceDB, ex-Amazon Fast Data, ex-Director of Engg. SRE and Data Engineering at Unacademy. I spark engineering curiosity through my no-fluff engineering videos on YouTube and my courses