The Two-Phase Commit Protocol in Apache Kafka

The Two-Phase Commit Protocol in Apache Kafka

How Kafka makes a group of writes across many partitions succeed or fail together: the transaction coordinator, the transaction log, prepare and completion markers, zombie fencing, read-committed consumers and the failure cases that make it all worth understanding.

Imagine you are paying a friend for a shared holiday. You must take money out of your account and your friend must receive it in theirs. If your bank removes the money but the transfer to your friend never arrives, you are poorer and nobody is happier. If the money arrives twice, your friend is happier and you are not. What you really want is a promise that either both steps happen or neither happens.

Kafka faces the same wish. An application may read a message, process it, and write results to several different topics and partitions, all spread across many machines. If it crashes halfway, some results exist and others do not. Kafka solves this with a transaction mechanism built around an old and famous idea from databases: the two-phase commit. This tutorial explains that idea and Kafka’s own version of it from the ground up, all the way to the advanced details that architects need in production.

1Why Atomic Writes Matter in Kafka

Before we meet any protocol, we must feel the problem it solves.

The world of many partitions

A Kafka topic is divided into partitions, and each partition is stored on a different broker as its leader. A single application may write to many partitions during a single piece of work. For example, an order-processing service might read one order, then write a payment event to one topic, an inventory event to another and a notification event to a third. Each of these writes goes to a different partition, often on a different machine.

Simple Analogy

Think of a wedding planner who must book a hall, a caterer and a band. If the hall and caterer are booked but the band is unavailable, the wedding cannot happen, and the planner would like to cancel the other two bookings. Planning only makes sense as a bundle: all three, or none.

What can go wrong without transactions

Failure 1

Partial writes

The application crashes after writing to two topics but before writing to the third. Downstream systems see an inconsistent picture.

Failure 2

Duplicate results

The application retries after a timeout and writes the same results a second time, because it cannot tell whether the first attempt succeeded.

Failure 3

Lost progress

The application writes results but crashes before recording that it has processed the input, so on restart it processes the input again.

Failure 4

Zombie instances

A stalled application instance is replaced by a new one, then wakes up and keeps writing, mixing its stale results with the new instance’s results.

The wish: exactly-once processing

What applications want is the effect of exactly-once processing: every input is reflected in the outputs precisely one time, even when machines crash and networks fail. Kafka’s transactions, combined with a few supporting features, are the foundation for delivering this within Kafka itself.

“A transaction is a promise that a bundle of writes will be seen entirely or not at all.”
i
Scope of the promise

Kafka’s transactions work across topics and partitions inside Kafka. They do not automatically cover side effects in outside systems such as databases or email services. We will return to this limit later, because it is one of the most important things to understand.

2Classic Two-Phase Commit in Plain English

The two-phase commit is one of the oldest ideas in distributed computing. Understanding the classic version makes Kafka’s version easy to see.

The cast of characters

In the classic protocol there is one coordinator and several participants. The coordinator is the one that wants a bundle of work done atomically. The participants are the independent parties that each perform a portion of that work, such as different databases.

Phase one: prepare and vote

The coordinator asks every participant: “Can you promise to commit your part, if I tell you to?” Each participant does everything it can short of the final commit, writes its promise to durable storage, and answers yes or no. A “yes” is a binding promise: from that moment the participant may not change its mind unless the coordinator says so.

Phase two: commit or abort

If every participant said yes, the coordinator decides to commit and tells all participants to complete. If anyone said no, or failed to answer, the coordinator decides to abort and tells all participants to undo their work. The decision is final and is recorded durably before any participant is told.

Simple Analogy

A group of friends organizing a surprise party. The organizer phones each friend: “Can you definitely come and bring your part?” Everyone says yes and promises to keep the evening free. Only then does the organizer announce, “It is on!” If even one friend says no, the organizer calls everyone: “It is off, forget it.” Nobody has half a party.

sequenceDiagram
    participant C as Coordinator
    participant P1 as Participant 1
    participant P2 as Participant 2
    C->>P1: Prepare - can you commit
    C->>P2: Prepare - can you commit
    P1-->>C: Yes, promise recorded
    P2-->>C: Yes, promise recorded
    C->>C: Record decision - commit
    C->>P1: Commit
    C->>P2: Commit
    P1-->>C: Done
    P2-->>C: Done
        
FIG 1 — The classic two-phase commit conversation
1

Ask

The coordinator asks each participant to prepare.

2

Promise

Each participant durably records its readiness and replies.

3

Decide

The coordinator durably records a single, final decision.

4

Tell

The coordinator instructs every participant to commit or abort.

5

Confirm

Participants finish and acknowledge, and the coordinator closes the transaction.

!
The point of no return

The moment the coordinator durably records its decision is the true moment the transaction is decided. Everything after that is just spreading the news reliably. Remember this idea, because it is exactly how Kafka works too.

3The Weakness of Classic Two-Phase Commit and Kafka’s Answer

The classic protocol has a famous problem, and Kafka’s design sidesteps most of it.

The blocking problem

Suppose a participant has said yes and is now waiting for the coordinator’s decision. If the coordinator crashes at that moment, the participant is stuck. It cannot commit, because the decision might have been abort. It cannot abort, because the decision might have been commit. It must wait, often holding locks that block other work, until the coordinator returns. This state is called being in doubt, and it is why classic two-phase commit is described as a blocking protocol.

Simple Analogy

You have promised to attend the surprise party and cleared your whole evening. The organizer suddenly becomes unreachable. You do not know if the party is on or off, so you sit at home, dressed up, unable to make other plans until the organizer calls back.

How Kafka changes the picture

Change 1

The coordinator is replicated

Kafka’s transaction coordinator keeps its decisions in a replicated log. If the coordinator broker fails, another broker takes over with the same recorded state and finishes the job.

Change 2

Participants do not vote

Partition leaders are not asked whether they can commit. They simply append whatever markers the coordinator sends. The decision rests entirely with the coordinator and the producer.

Change 3

Data is written before the decision

The producer writes the actual records into the partitions during the transaction, so by commit time all the heavy work is already durable. Commit only reveals it.

Change 4

Timeouts bound the waiting

A transaction that stays open too long is aborted by the coordinator, so readers are not held up forever.

A different flavor of the same idea

It is fair to say that Kafka implements a two-phase commit variant rather than the textbook version. The important features remain: a durable decision point, a second phase that spreads the decision to all involved partitions, and completion that is guaranteed to happen eventually once the decision is recorded. The absence of a voting phase and the presence of a replicated coordinator make it far more practical for a streaming platform.

AspectClassic two-phase commitKafka transactions
Coordinator durabilityCoordinator’s own log, usually one machineReplicated transaction log with failover
Participant votingYes, each participant votesNo voting, participants apply markers
When data is writtenDuring or after prepareDuring the transaction, before prepare
Decision pointCoordinator records commit or abortCoordinator records prepare-commit or prepare-abort
How readers see resultsLocks hide uncommitted dataMarkers and read-committed filtering hide uncommitted data
Blocking riskHigh when the coordinator failsLower, recovered by the new coordinator, bounded by timeouts
i
Keep the classic picture in mind

Even though details differ, every step of Kafka’s protocol maps onto the classic story: prepare, decide durably, tell everyone, confirm. When a step seems confusing, translate it back to the surprise-party analogy.

4The Cast of a Kafka Transaction

Five kinds of participants make Kafka transactions work. Meeting them first makes the later story easy to follow.

Actor 1

Transactional producer

The application that begins a transaction, writes records, and finally asks to commit or abort. It carries a stable name called the transactional identifier.

Actor 2

Transaction coordinator

A component running inside a broker. It tracks each transaction’s state, decides the outcome and drives the completion.

Actor 3

Transaction log

An internal, replicated topic where the coordinator durably records every state change of every transaction.

Actor 4

Partition leaders

The brokers holding the partitions the transaction writes to. They store records and later store the commit or abort markers.

Actor 5

Consumers

Applications that read the results. Those set to read committed data only see records from finished, committed transactions.

flowchart LR
    P["Transactional producer"] -->|"1 register and coordinate"| TC["Transaction coordinator"]
    TC -->|"2 record state"| TL["Transaction log topic"]
    P -->|"3 write records"| L1["Partition A leader"]
    P -->|"3 write records"| L2["Partition B leader"]
    TC -->|"4 write markers"| L1
    TC -->|"4 write markers"| L2
    L1 --> C["Read-committed consumer"]
    L2 --> C
        
FIG 2 — Who talks to whom in a Kafka transaction
Simple Analogy

Consider a courier company handling a multi-parcel delivery. The customer (producer) hands over parcels to several local depots (partition leaders). A head office clerk (coordinator) keeps a ledger (transaction log) of the shipment. At the end, the clerk sends a stamp to every depot saying “delivered” or “cancelled”. Recipients (consumers) only accept parcels that carry the “delivered” stamp.

The coordinator is a role, not a separate server

There is no separate transaction server to install. Every broker can act as a transaction coordinator for some share of the transactional identifiers. Which broker is responsible for a particular transactional identifier is decided by a simple rule, described in a later chapter, so both the producer and the cluster always agree on who the coordinator is.

i
Everything is built from ordinary parts

The transaction log is a normal Kafka topic, the markers are normal records with a special flag, and the coordinator is normal broker code. Kafka reused its own strengths of replication and ordered logs to implement transactions.

5Foundation One: The Idempotent Producer

Transactions are built on top of a simpler feature that removes duplicates caused by retries.

The retry problem

A producer sends a batch of records to a broker and waits for an acknowledgement. If the acknowledgement never arrives, the producer does not know whether the broker stored the batch. To be safe it retries. But if the first attempt had actually succeeded and only the reply was lost, the retry stores the batch a second time, creating duplicates.

The idempotent solution

An idempotent producer attaches two numbers to every batch: a producer identifier assigned by the cluster, and a sequence number that increases by one for each batch sent to a given partition. The broker remembers the latest sequence numbers it has accepted from each producer for each partition. If a batch arrives with a sequence number it has already stored, the broker recognizes the duplicate and quietly discards it while still acknowledging success.

Simple Analogy

A postal clerk stamps each of your letters with a running number. If you accidentally hand the clerk letter number seven twice, the clerk notices the number was already processed and just smiles and says “already sent”. Your friend receives it once.

Ingredient 1

Producer identifier

A number, often called the PID, that the cluster assigns to identify one producer session.

Ingredient 2

Producer epoch

A small counter attached to the identifier that increases when the identity is renewed or a newer instance takes over. It is the key to fencing old instances.

Ingredient 3

Sequence numbers

A per-partition counter that lets the broker detect duplicates and out-of-order batches.

Ingredient 4

Acknowledgement from all in-sync replicas

Required so that acknowledged batches survive the failure of a leader.

What idempotence does and does not cover

Covered

  • No duplicates in a single partition caused by producer retries within one session
  • Correct ordering per partition even with several requests in flight
  • Automatic detection of gaps that indicate lost data

Not covered

  • Atomic writes across several partitions or topics
  • Duplicates caused by the application restarting and resending on its own
  • Coordination between the producer’s writes and consumer offsets
  • Continuity of identity after the producer process restarts
!
Why we need more

Idempotence works within a single producer session and a single partition. To make many writes atomic across partitions, and to keep a stable identity across restarts, Kafka adds the transactional identifier and the coordinator. That is the next step.

6Foundation Two: Transactional ID and Zombie Fencing

A stable name for the producer, plus an ever-growing epoch, protects the system from stale instances.

The transactional identifier

A transactional producer is configured with a name called the transactional identifier. This is chosen by the application, not by Kafka, and it must stay the same across restarts of the same logical worker. The cluster uses it to remember which transaction belongs to which worker, and to link the worker’s identity across different sessions.

Simple Analogy

A security guard has a permanent staff badge number. Every day a new shift begins with a fresh daily pass, but the badge number stays the same. The gatehouse uses the badge number to know which guard is which, even after a shift change.

What happens when a producer starts

1

Find the coordinator

The producer asks any broker which broker is the transaction coordinator for its transactional identifier.

2

Initialize the producer identity

The producer asks the coordinator to initialize its identity. The coordinator assigns or looks up the producer identifier for this transactional identifier.

3

Raise the epoch

The coordinator increases the epoch, so any older instance that used the same transactional identifier now holds an outdated epoch.

4

Clean up old work

If the previous instance left a transaction open, the coordinator aborts it, or finishes a commit that was already decided, before letting the new instance continue.

5

Ready to work

The producer now holds a fresh identity and can begin transactions.

What is a zombie?

A zombie is an old application instance that the rest of the system believes is dead, but which is actually still alive. It may have been paused by a long garbage collection, frozen by a virtual machine migration, or disconnected by a network partition. A replacement instance has already started. If the zombie wakes up and resumes writing, two instances do the same job at once, and results are duplicated or interleaved.

sequenceDiagram
    participant Z as Old instance - epoch 4
    participant N as New instance
    participant TC as Coordinator
    participant PL as Partition leader
    Z->>Z: Long pause
    N->>TC: Initialize with same transactional id
    TC->>TC: Raise epoch to 5, abort old transaction
    TC-->>N: Producer id, epoch 5
    Z->>Z: Wakes up
    Z->>PL: Write with epoch 4
    PL-->>Z: Rejected - fenced by newer epoch
    N->>PL: Write with epoch 5
    PL-->>N: Accepted
        
FIG 3 — A stale instance is fenced by the newer epoch

The fence in action

Every write by a transactional producer carries its epoch. Brokers and the coordinator compare it with the latest epoch known for that identifier. A request with an older epoch is refused with an error that tells the producer it has been fenced. The producer is expected to treat this as a fatal error: it must stop, discard its work, and let the new instance take over. This guarantees that at any moment only one live instance can make progress for a given transactional identifier.

!
Do not share identifiers between workers

Two workers doing different jobs must never use the same transactional identifier. They would keep fencing each other out. Each logical worker gets its own identifier that persists across its restarts.

Stable assignment in stream processing

Frameworks that process partitioned input typically derive the transactional identifier from the partition or task being processed, so that whichever instance takes over that work uses the same identifier and fences its predecessor.

7The Transaction Log and How the Coordinator Is Chosen

The coordinator’s memory lives in an internal topic. Its layout explains both the durability and the failover behavior.

An internal topic with many partitions

The transaction log is an internal Kafka topic with a fixed number of partitions, fifty by default. It is replicated across brokers just like any topic, and is typically configured with a higher replication factor and a strict minimum number of in-sync replicas to protect it against data loss. Because it is a normal replicated topic, its content survives the failure of individual brokers.

Choosing the coordinator by hashing

To decide which broker coordinates a given transactional identifier, Kafka hashes the identifier and maps it to one of the partitions of the transaction log. The broker that currently leads that partition is the coordinator for the identifier. There is nothing to configure and no separate election. If the partition’s leadership moves to another broker, the coordinator role moves with it.

Simple Analogy

A large library sorts membership records into fifty drawers according to the first letters of each member’s surname. Each drawer has one clerk in charge. If a clerk falls ill, another clerk takes over the same drawer with all the same cards inside it.

flowchart TB
    T1["Transactional id: orders-worker-1"] -->|"hash modulo partitions"| P7["Transaction log partition 7"]
    T2["Transactional id: billing-worker-3"] -->|"hash modulo partitions"| P31["Transaction log partition 31"]
    P7 --> B2["Leader on broker 2 - coordinator"]
    P31 --> B5["Leader on broker 5 - coordinator"]
        
FIG 4 — Mapping a transactional identifier to its coordinator

What is stored in the log

Field

Transactional identifier

The key, so newer entries replace older ones for the same worker when the log is compacted.

Field

Producer identifier and epoch

The current identity of the worker, used for fencing.

Field

Transaction timeout

How long the worker is allowed to keep a transaction open.

Field

Current state

Where the transaction is in its life, such as ongoing or preparing to commit.

Field

Involved partitions

The list of topic partitions that the transaction has written to so far.

Field

Last update time

Used to detect transactions that have been open for too long.

Compaction and recovery

The transaction log is compacted, meaning only the latest entry for each transactional identifier is eventually kept. This keeps it small. When a broker becomes the leader of a transaction log partition, its coordinator reads the partition from start to end to rebuild an in-memory table of every transaction it is responsible for. Any transaction found in the middle of committing or aborting is then driven forward to completion. This loading step is how Kafka avoids the classic blocking problem.

i
Why durability settings matter

If the transaction log loses data, the cluster could forget a decision it had already made. That is why the transaction log has stricter replication defaults than ordinary topics, and why those defaults should not be weakened casually.

8The Life Cycle States of a Transaction

Each transaction moves through a small set of states. Knowing them turns error messages and tool outputs into plain sentences.

State

Empty

No transaction is active for this identifier. The worker is registered and idle.

State

Ongoing

A transaction is open. Partitions may be added and records written.

State

Prepare commit

The coordinator has durably decided to commit, but markers have not all been written yet.

State

Prepare abort

The coordinator has durably decided to abort, but markers have not all been written yet.

State

Complete commit

All commit markers are written. The transaction is finished and its data is visible.

State

Complete abort

All abort markers are written. The transaction is finished and its data stays hidden.

State

Dead

The identifier has expired after a long period of inactivity and its record is being removed.

State

Prepare epoch fence

A short internal state used while an older instance is being fenced out and its unfinished transaction cleaned up.

stateDiagram-v2
    [*] --> Empty
    Empty --> Ongoing: first partition added
    Ongoing --> PrepareCommit: producer requests commit
    Ongoing --> PrepareAbort: producer requests abort or timeout
    PrepareCommit --> CompleteCommit: all commit markers written
    PrepareAbort --> CompleteAbort: all abort markers written
    CompleteCommit --> Empty: ready for next transaction
    CompleteAbort --> Empty: ready for next transaction
    Empty --> Dead: identifier expires
        
FIG 5 — Main state transitions of a transaction

Reading the states as a story

The two prepare states are the heart of the two-phase idea. Entering a prepare state means the decision has been made and written down. Everything that follows is mechanical: send markers, wait for confirmations, write the completion state. If anything crashes, whoever takes over reads the prepare state from the log and simply carries on.

Simple Analogy

A restaurant order slip travels from “taken” to “being cooked” to “served”. If the waiter goes home sick, a colleague reads the slip and knows exactly which stage the order has reached, and continues from there.

!
A prepare state must never be undone

Once a transaction reaches prepare commit, it can only move to complete commit. Once it reaches prepare abort, it can only move to complete abort. This one-way rule is what makes recovery safe.

9A Transaction From Start to Finish

Now we combine every idea and walk through one successful transaction, step by step.

Suppose a transactional producer must write one record to topic orders partition zero and another record to topic payments partition three, atomically, as one bundle.

1

Initialize

At startup the producer finds its coordinator and obtains its identity and epoch, as described earlier. This happens once per producer session.

2

Begin

The application marks the start of a transaction. This is a local step: nothing is sent yet, the producer simply remembers that a transaction is open.

3

Register partitions

Before sending the first record to each new partition, the producer tells the coordinator which partition it is about to write to. The coordinator records it in the transaction log, moving the transaction to the ongoing state.

4

Write records

The producer sends its records to the partition leaders. They are stored in the partition logs exactly like normal records, but flagged as belonging to an open transaction.

5

Request commit

The application asks to commit. The producer flushes any pending records and sends a commit request to the coordinator.

6

Phase one: durable decision

The coordinator writes “prepare commit” to the transaction log and waits until it is replicated. This is the point of no return. It then answers the producer that the commit has been accepted.

7

Phase two: markers

The coordinator sends a commit marker to the leader of each partition involved. Each leader appends the marker to its log. Once the marker is written, the records become visible to read-committed consumers.

8

Complete

After every leader has confirmed its marker, the coordinator writes “complete commit” to the transaction log. The producer can begin its next transaction.

sequenceDiagram
    participant P as Producer
    participant TC as Coordinator
    participant TL as Transaction log
    participant A as Orders partition 0 leader
    participant B as Payments partition 3 leader
    P->>TC: Add partitions orders-0 and payments-3
    TC->>TL: Record state ongoing with partitions
    P->>A: Write order record
    P->>B: Write payment record
    P->>TC: Commit
    TC->>TL: Write prepare commit
    TL-->>TC: Replicated
    TC-->>P: Commit accepted
    TC->>A: Write commit marker
    TC->>B: Write commit marker
    A-->>TC: Marker written
    B-->>TC: Marker written
    TC->>TL: Write complete commit
        
FIG 6 — A successful two-partition transaction
i
The producer does not wait for markers

The producer’s commit call returns once the decision is durably recorded, which is phase one. The coordinator spreads the decision to partitions in the background. From the producer’s viewpoint, the decision is already final at that moment.

10Phase One: The Prepare Step and the Decision Point

Everything important happens in one small durable write.

What the coordinator does

When the producer asks to commit, the coordinator first checks that the request comes from the current epoch, that the transaction is in the ongoing state, and that its recorded list of partitions is complete. It then writes a single entry to the transaction log saying the transaction is now in the prepare-commit state, together with the full list of partitions involved. It waits until this entry has been copied to the required in-sync replicas of the transaction log.

Why this write is the decision

After this entry is safely stored, no failure can change the outcome. If the coordinator broker crashes, another broker will become leader of that transaction log partition, read the entry, and know for certain that the decision was commit. There is no scenario in which the transaction can later become an abort. Conversely, if the entry was not stored, then the transaction was never decided, and a recovered coordinator will treat it as still ongoing, and it will eventually time out and be aborted.

Simple Analogy

A judge signs a verdict on paper and files it in the court archive. Even if the judge leaves the building at once, clerks can read the filed verdict and carry out the sentence. Until the signature is filed, there is no verdict.

What happened before the crashWhat the recovered coordinator concludesWhat it does next
Nothing written for this transactionNo transaction is knownWaits for the producer to reinitialize
Ongoing state written, no prepare entryThe transaction was not decidedWaits for the producer, or aborts on timeout
Prepare commit entry writtenThe decision was commitSends commit markers and completes
Prepare abort entry writtenThe decision was abortSends abort markers and completes
Complete entry writtenEverything is finishedNothing more to do

The difference from classic voting

In the classic protocol, this is where participants would each vote. In Kafka, no such vote is needed. The partitions have already stored the transaction’s records, and there is nothing further they must check. The prepare step is therefore quick and involves only the coordinator and its replicated log.

!
Durability settings are part of correctness

The safety of the decision depends on the transaction log being properly replicated. Running it with a single replica or a relaxed minimum in-sync setting removes the guarantee that a recorded decision survives a broker failure.

11Phase Two: Markers and Completion

With the decision safely recorded, the coordinator spreads the news to every partition involved.

The marker request

The coordinator sends a special request to the leader of each partition listed in the transaction. The request says, in effect, “write a marker for this producer identifier and epoch saying that its transaction committed” or “aborted”. Several partitions may share one broker, so the coordinator batches these requests per broker for efficiency.

What the partition leader does

On receiving the request, the leader appends a special control record to the partition log. This marker is a small batch flagged as a control batch. It is replicated to the followers of the partition like any other record. When the marker has been replicated, the leader acknowledges to the coordinator. The marker is a permanent part of the partition log and acts as the official “commit” or “abort” stamp for all earlier records from that producer’s transaction.

Simple Analogy

A publishing house prints a batch of books, but they are kept in the warehouse until a stamp saying “approved for sale” or “recalled” is pressed onto the shipping list. Shops only stock books once the approved stamp appears. The stamp does not change the books, only their status.

Retrying until done

If a partition leader is temporarily unavailable, or the request times out, the coordinator simply retries, sending the request again to whichever broker now leads the partition. Because the decision is already recorded, retries continue until every partition acknowledges. Marker writes are safe to repeat: a repeated marker is recognized and does no additional harm.

1

Group by broker

The coordinator collects the involved partitions and groups them by the broker currently leading each one.

2

Send marker requests

One request per broker carries all the markers that broker must write.

3

Append and replicate

Each leader appends control records and replicates them to its followers.

4

Collect acknowledgements

The coordinator tracks which partitions have confirmed and retries the rest.

5

Write the completion state

When all partitions have confirmed, the coordinator records the transaction as complete in the log.

i
Visibility is per partition

Markers are written partition by partition, not all at the same instant. For a short moment, a committed transaction may be visible in one partition and not yet in another. The guarantee is that it will become visible in all of them, and it will never be visible in some and permanently hidden in others.

12Control Records Inside Partition Logs

The partition log itself carries the evidence of every transaction outcome.

Data records and control records

A partition that takes part in transactions contains two kinds of batches. Data batches hold the actual records written by producers. Control batches hold small markers written by the coordinator. Control batches carry a flag in their header, so readers can tell them apart from data instantly. Applications never see control records: the client library reads them, uses them to decide what to show, and then hides them.

Batch

Transactional data batch

Contains user records. Its header carries the producer identifier, epoch and a flag saying it belongs to a transaction.

Batch

Commit marker

A control batch declaring that the producer’s transaction has committed. It uses one offset in the log.

Batch

Abort marker

A control batch declaring that the producer’s transaction has aborted.

Batch

Ordinary data batch

Records from non-transactional producers, which are visible as soon as they are stored.

Offsets and gaps

Because each marker occupies a position in the log, offsets seen by consumers sometimes appear to skip a number. This is normal: the skipped offset belongs to a marker the consumer does not receive. Applications must never assume offsets are perfectly consecutive. Aborted records also keep their offsets in the log, although they are filtered out for read-committed consumers.

flowchart LR
    R1["Offset 10 - record from txn A"] --> R2["Offset 11 - record from txn B"]
    R2 --> R3["Offset 12 - record from txn A"]
    R3 --> M1["Offset 13 - commit marker for A"]
    M1 --> R4["Offset 14 - ordinary record"]
    R4 --> M2["Offset 15 - abort marker for B"]
        
FIG 7 — Interleaved transactions and their markers in one partition
Simple Analogy

A shared notebook contains notes from several people written in the order they came. Now and then a teacher writes a stamp on a line: “Alice’s notes above are approved” or “Bob’s notes above are cancelled”. Readers use the stamps to decide which notes count, without ever erasing anything.

!
Interleaving is normal

A partition can hold data from several open transactions at the same time, mixed with ordinary records. The log is never split into per-transaction sections. Markers and metadata sort everything out when reading.

13Read Committed, the Last Stable Offset and Aborted Data

Consumers choose what they want to see, and brokers help them see the right thing efficiently.

Two isolation levels

Read committed

  • Shows records from committed transactions and ordinary records
  • Hides records from aborted transactions
  • Holds back records from still-open transactions
  • Required to benefit from transactional guarantees

Read uncommitted

  • Shows every record as soon as it is stored
  • Includes records from open and aborted transactions
  • Lowest latency, no waiting for transactions to finish
  • The default setting, so it must be changed deliberately

The last stable offset

For each partition, the broker tracks a position called the last stable offset. It is the offset of the earliest record that still belongs to a transaction that has not finished, or the end of the log if there is no such transaction. A read-committed consumer is never given anything at or beyond this position. Everything before it is settled: either committed or aborted or not transactional.

Simple Analogy

A newspaper only prints facts that have been confirmed. A story that is still being checked holds up every later story in the same column, even if those are already verified, because the column is printed in order. The line just before the unverified story is the last stable position.

flowchart LR
    A["Offsets 0 to 20 - all settled"] --> B["Offset 21 - first record of open transaction"]
    B --> C["Offsets 22 to 30 - later records"]
    A -. "read committed consumers can read up to here" .-> LSO["Last stable offset = 21"]
    C --> HW["High watermark = 31"]
        
FIG 8 — The last stable offset sits before the earliest open transaction

Aborted transaction index

To let consumers skip aborted data quickly, brokers keep an index of aborted transactions for each partition segment. When a consumer fetches a range of records, the broker also returns a list of the aborted transactions that overlap that range. The client library uses this list, together with markers, to drop aborted records before handing data to the application. The consumer does not need to read the whole log to know what to skip.

Offset boundaryMeaningWho cares
Log start offsetOldest record still retainedRetention and consumers starting from the beginning
Last stable offsetEverything before it is settledRead-committed consumers
High watermarkEverything before it is replicated to all in-sync replicasRead-uncommitted consumers
Log end offsetNext offset to be writtenLeaders and replication
!
One slow transaction delays everyone behind it

Because the last stable offset stops at the earliest open transaction, a single long-running transaction can make read-committed consumers wait, even for unrelated records that follow. This is why transaction timeouts and short transactions matter so much.

14Consume, Transform, Produce: Offsets Inside the Transaction

The most common use of transactions is to read input, compute, and write output, all as one atomic unit.

The problem of the input position

A stream processing application reads messages from an input topic, transforms them and writes results to an output topic. To avoid reprocessing on restart, it records how far it has read by committing the consumer offset. If the output write and the offset commit are separate steps, a crash between them leaves either a duplicate output or a lost input.

Making the offset commit part of the transaction

Kafka lets a transactional producer include the consumer’s offsets inside the transaction. The offsets are written to the internal offsets topic as part of the same transaction as the output records. When the transaction commits, both the outputs and the new input position become visible together. When it aborts, neither does, and the application restarts from the previous position and redoes the work cleanly.

Simple Analogy

A clerk moves invoices from an in-tray to an out-tray. Moving an invoice and ticking it off on the tally sheet are done together as one action. If the clerk is interrupted mid-way, both are undone, so the tally never says an invoice was handled when it was not.

1

Poll input

The consumer reads a batch of input records, with automatic offset commits turned off.

2

Begin the transaction

The producer marks the start of a new transaction.

3

Write outputs

Transformed results are sent to one or more output topics inside the transaction.

4

Attach the offsets

The producer sends the consumer’s latest positions and its group information to the coordinator, registering the offsets partition as part of the transaction.

5

Commit

The producer commits. Outputs and offsets become visible together through the two-phase process.

flowchart LR
    IN["Input topic"] --> CO["Consumer"]
    CO --> APP["Application logic"]
    APP --> PR["Transactional producer"]
    PR -->|"outputs"| OUT["Output topics"]
    PR -->|"consumer offsets"| OFF["Offsets topic"]
    PR -->|"commit or abort"| TC["Coordinator"]
        
FIG 9 — Outputs and input position committed as one unit
i
Group membership checks

The producer passes the consumer’s group metadata when attaching offsets. This lets the cluster reject offset commits from a member that has already been kicked out of the group, adding another layer of protection against zombie consumers.

15The Abort Path

Half of a good protocol is what it does when things do not go to plan.

Ways an abort begins

Trigger

The application aborts

The application catches an error during processing and explicitly asks to abort the transaction.

Trigger

Timeout

The transaction has been open longer than its allowed time, and the coordinator aborts it on its own.

Trigger

Fencing

A newer instance with the same transactional identifier initializes, and the coordinator aborts the older instance’s open transaction.

Trigger

Fatal producer error

The producer hits an error it cannot recover from and is expected to abort or shut down.

The mirror image of commit

Aborting follows exactly the same two-phase pattern as committing. The coordinator writes prepare-abort to the transaction log, which is the decision point. It then sends abort markers to all involved partitions, and finally writes complete-abort. The records that were already written to the partitions stay in the log physically, but the abort markers and the aborted transaction index make read-committed consumers ignore them.

sequenceDiagram
    participant P as Producer
    participant TC as Coordinator
    participant TL as Transaction log
    participant L as Partition leaders
    P->>TC: Abort
    TC->>TL: Write prepare abort
    TL-->>TC: Replicated
    TC-->>P: Abort accepted
    TC->>L: Write abort markers
    L-->>TC: Markers written
    TC->>TL: Write complete abort
        
FIG 10 — The abort path mirrors the commit path
Simple Analogy

Cancelling a wedding follows a mirror of arranging it. You decide, write the decision down, and send cancellation notices to the hall, the caterer and the band. Everyone who received the earlier bookings now receives a stamp saying “cancelled” and stops treating them as real.

Aborted data still costs storage

The rejected records remain in the partition until normal retention removes them. A workload with a very high abort rate therefore wastes disk and network bandwidth, and this is a useful signal to investigate.

Application retry after abort

After an abort, a well-written application rewinds its input position to the last committed offsets and processes the same input again in a fresh transaction.

16Failure Scenarios One by One

A protocol proves itself on the worst day. Here is what happens when each part fails.

Scenario: the producer crashes mid-transaction

The transaction stays open. After the transaction timeout, the coordinator aborts it and writes abort markers. The records written so far become invisible to read-committed consumers. When a replacement producer starts with the same transactional identifier, it is given a fresh epoch and begins cleanly.

Scenario: the producer crashes right after asking to commit

If the prepare-commit entry was stored, the coordinator carries the commit through to completion no matter what happens to the producer. If it was not stored, the transaction was never decided and will be aborted on timeout. The producer’s replacement, on initialization, sees the final result.

Scenario: the coordinator broker crashes before deciding

Leadership of the transaction log partition moves to another broker, which loads the log. It finds the transaction ongoing. The producer’s next request fails with a retriable error, the producer finds the new coordinator and continues or retries the commit request.

Scenario: the coordinator crashes after recording prepare-commit

The new coordinator loads the log, sees prepare-commit with the list of partitions, and sends the commit markers itself. The transaction completes as though nothing happened. This is the exact situation where the classic protocol would have blocked.

Scenario: a partition leader fails while markers are being written

The coordinator’s marker request fails or times out. It retries against the new leader after election. Because the marker write is safe to repeat, the transaction eventually completes.

Scenario: network partition isolates an old producer

The coordinator times out the old transaction and a replacement instance takes over with a higher epoch. When the old producer’s network returns, all its requests are rejected because its epoch is outdated, and it shuts down.

Scenario: a consumer fails while reading committed data

The consumer restarts from its last committed offset and reads again. Because it only sees settled data, it never observes half of a transaction.

FailureOutcome for the transactionWho resolves it
Producer dies before commit requestAbortedCoordinator by timeout, or new instance by fencing
Producer dies after decision recordedCompletes as decidedCoordinator
Coordinator dies before decisionStill open, later committed or abortedNew coordinator with the producer
Coordinator dies after decisionCompletes as decidedNew coordinator
Partition leader dies during markersCompletes after retryCoordinator
Zombie producer resurfacesIts writes are rejectedEpoch fencing
i
The pattern to remember

Before the decision is recorded, failure leads to abort. After the decision is recorded, failure leads to completion of that decision. Nothing ever leads to a half-and-half result.

17Coordinator Recovery in Detail

The recovery routine is the reason this protocol is safer than its textbook ancestor.

When a broker becomes a coordinator

When a broker becomes leader for a partition of the transaction log, it runs a loading routine. It reads the partition from the beginning, applying each entry in order to build an in-memory table with the latest state of every transactional identifier that maps to that partition. During this time it refuses to serve requests for those identifiers and tells clients to retry shortly.

1

Become leader

The broker is chosen as leader for a partition of the transaction log.

2

Read the log

It reads every entry in the partition and builds a table of current transaction states.

3

Find unfinished decisions

Entries in a prepare state indicate a decision that still needs its markers written.

4

Resume phase two

For each prepare entry, it sends the corresponding markers to the recorded partitions and finishes the transaction.

5

Start timers

It starts watching ongoing transactions so that any that exceed their timeout are aborted.

6

Open for business

The coordinator begins serving requests for its identifiers again.

flowchart TB
    A["Coordinator broker fails"] --> B["Another broker becomes leader of the log partition"]
    B --> C["Load and replay the partition"]
    C --> D{"Any transaction in a prepare state?"}
    D -->|"yes"| E["Send markers and write completion"]
    D -->|"no"| F["Keep ongoing ones under timeout watch"]
    E --> G["Serve requests"]
    F --> G
        
FIG 11 — Recovery when a new coordinator takes over
Simple Analogy

A night-shift manager takes over a warehouse. Before doing anything else, the manager reads the shift log, finds any shipments marked “approved, not yet dispatched”, and dispatches them. Then the manager goes on to normal work. Nothing that was already approved is lost.

!
Loading takes time

The time to load depends on the size of the log partition after compaction. Very large numbers of active transactional identifiers, or a poorly compacted log, can lengthen the period during which transactions for those identifiers see retriable errors after a failover.

18Timeouts and Hanging Transactions

Time limits are the safety valve that keeps a broken transaction from holding up the rest of the system.

The transaction timeout

Every transactional producer declares how long it is willing to keep a transaction open. The coordinator watches the clock. If a transaction remains open beyond its timeout without a commit or abort request, the coordinator aborts it. The broker also enforces an upper limit on what a producer may ask for, so no client can request an unreasonably long timeout.

Why timeouts are essential

Recall that the last stable offset stops at the earliest open transaction, and read-committed consumers cannot read past it. A crashed producer that left a transaction open would otherwise freeze every read-committed consumer of that partition forever. The timeout guarantees that the freeze lasts at most a bounded time.

Simple Analogy

A meeting room can be booked, but if the organizer does not turn up within fifteen minutes, the booking is cancelled so that others can use it. Without the rule, one no-show could lock the room all day.

Hanging transactions

Occasionally, because of bugs or unusual failure sequences, a transaction can remain open on a partition even though the coordinator no longer considers it active. Such a transaction is called hanging. It holds the last stable offset back and causes consumer lag to grow while data keeps arriving. Kafka provides an administrative tool to find such transactions and to abort them safely.

Symptom

Read-committed lag climbs

Lag for read-committed consumers rises, while read-uncommitted consumers of the same partition keep up.

Symptom

Last stable offset is stuck

The gap between the high watermark and the last stable offset grows and never shrinks.

Response

List open transactions

Use the transactions tool to list and describe transactions, and to search for hanging ones on a suspect partition.

Response

Abort the hanging one

Use the same tool to abort a confirmed hanging transaction, after checking that it is truly abandoned.

!
Be certain before aborting

Aborting a transaction that is genuinely still in use will discard the work of a healthy producer. Confirm from the coordinator’s view and from the application that the transaction is really abandoned first.

Choosing a sensible timeout

Set the timeout comfortably longer than your longest normal transaction, but not so long that a failure freezes consumers for minutes. Many teams start near the default and adjust based on measured transaction durations.

Expiration of idle identifiers

Separately, transactional identifiers that have been idle for a long period are expired and removed from the log, keeping it small. A worker that returns after expiry simply obtains a fresh identity.

19What Exactly-Once Really Means, and What It Does Not

Exactly-once is a powerful phrase and a frequently misunderstood one. Precision here saves real money and reputation.

The boundary of the guarantee

Kafka’s exactly-once guarantee means that, within a pipeline that reads from Kafka topics and writes to Kafka topics, each input record affects the output exactly once, even when processes and brokers fail and recover. It is achieved by combining idempotent writes, atomic multi-partition transactions with offsets, zombie fencing and read-committed consumers.

Inside the guarantee

  • Reading from Kafka topics and writing to Kafka topics
  • Committing input positions together with outputs
  • Fencing of stale producer instances
  • Consumers reading only committed results

Outside the guarantee

  • Writes to external databases, services and files
  • Emails, payments or other real-world side effects
  • Consumers configured to read uncommitted data
  • Business logic that is itself not deterministic or safe to repeat
Simple Analogy

A bank guarantees that a transfer between two accounts inside the bank happens exactly once. It cannot guarantee that a courier who was told to hand over cash at a shop will not deliver it twice. The guarantee ends where the bank’s own books end.

The trap of external side effects

Suppose an application inside a transaction calls an external payment service. If the transaction later aborts and the input is reprocessed, the payment service is called a second time. Kafka cannot undo the first call. The application must therefore make external calls safe to repeat, for example by sending a unique request identifier that the external service uses to ignore duplicates.

1

Keep external calls out of the transaction where possible

Emit an event to Kafka inside the transaction and let a separate step perform the external action from that event.

2

Make the external call idempotent

Give each action a stable identifier so repeating it has no additional effect.

3

Record what was done

Track completed actions in a durable store so a retry can skip what has already happened.

!
Never advertise more than you guarantee

When describing a pipeline as exactly-once, state its boundary clearly. A pipeline that calls outside systems is at best exactly-once within Kafka and at-least-once or effectively-once beyond it.

20Transactions in Stream Processing and Connectors

Most people meet Kafka transactions through higher-level tools that do the plumbing for them.

Stream processing libraries

Kafka’s own stream processing library offers an exactly-once processing mode that wraps every processing step in the consume, transform and produce transaction pattern described earlier. It commits regularly on a short interval, keeps its internal state changelogs inside the same transactions, and manages transactional identifiers and fencing automatically. Users select the mode through a single setting and do not touch producers or coordinators directly.

Feature

Commit interval

Transactions are committed on a regular short interval. A shorter interval reduces latency and duplicate work after failure, but raises transaction overhead.

Feature

State store changelogs

Updates to local state are written to changelog topics inside the same transaction, so state and output stay consistent.

Feature

Automatic identifiers

The library assigns stable transactional identifiers to processing tasks so that a replacement worker fences its predecessor.

Feature

Read committed input

Input consumers are set to read committed data so upstream aborts are respected.

Connector frameworks

Connector frameworks can provide exactly-once delivery for certain source and sink connectors, where the connector and the external system support the necessary coordination. For sources, the framework can commit source records and their source offsets together in one transaction. For sinks, exactly-once outcomes usually rely on the external system accepting idempotent or transactional writes, since Kafka’s transaction cannot extend into it.

Aggregations and joins

Counting or summing over event streams is where duplicates hurt most. Exactly-once processing keeps counts correct across restarts.

Multi-topic event fan-out

One input event producing several output events, such as an order producing payment, shipment and audit events, benefits from all-or-nothing publication.

Change data pipelines

Moving data between clusters or systems is safer when each batch and its position are committed together.

i
Prefer the framework

Hand-rolling the consume, transform and produce loop with transactions is easy to get subtly wrong. If your job fits a stream processing library, use its exactly-once mode and let it handle identifiers, fencing and offsets.

21Hardening the Protocol: Recent Improvements

The transaction protocol has been refined over the years to close subtle gaps found in real deployments.

The gap that motivated the changes

In the original design, a delayed write from a producer could arrive at a partition leader after the transaction it belonged to had already been completed. The leader, unaware, might store it as though it were part of a new, still-open transaction or leave it dangling. This class of problem is sometimes called a hanging transaction, and it is very hard to detect, since every individual step looked correct at the time it happened.

Simple Analogy

A late letter arrives at a company after the deal it refers to was already closed. A careless mailroom clerk files it into the current deal folder, confusing everyone. A careful clerk checks the reference number against the open deals first, and rejects letters that refer to closed ones.

Two kinds of improvement

Improvement

Server-side verification

Partition leaders confirm with the coordinator’s information that a partition really belongs to the current transaction before accepting the first transactional write for it. Late or stray writes are refused.

Improvement

Epoch bump on every transaction

In newer protocol versions, the producer’s epoch increases each time a transaction completes. A straggling request from a previous transaction therefore carries an outdated epoch and is fenced.

Improvement

Fewer round trips

Registration of partitions with the coordinator can be folded into the first write, so the producer no longer needs a separate step before each new partition.

Improvement

Clearer error handling

Errors are classified more precisely into retriable, abortable and fatal, so applications can respond correctly.

Older behaviorNewer behaviorBenefit
Partition leader accepts a transactional write on trustLeader verifies the partition belongs to the open transactionStops late writes from corrupting later transactions
Epoch changes only at initializationEpoch changes at each transactionStale requests from earlier transactions are fenced
Separate partition registration request before the first writeRegistration implied by the first writeLower latency per transaction
Some error cases ambiguousErrors clearly categorizedCorrect retry or abort behavior in clients
i
Version and feature dependencies

These improvements depend on both the broker release and a cluster-level transaction feature level, and on the client library version. Check the release notes and upgrade guide for your versions to learn when the newer transaction protocol is available and how it is enabled.

!
Keep clients current

Older clients continue to work with newer brokers, but they may not benefit from the stronger guarantees. Teams that depend on transactions should treat client library upgrades as part of their reliability work.

22Two-Phase Commit With Outside Systems

Real applications must often update a database and Kafka together. This is where transactions meet their limits, and where new proposals and older patterns help.

The dual-write problem

An application that saves an order to a database and then publishes an order event to Kafka is doing two writes to two systems. If it crashes between them, either the database has the order but Kafka does not, or the reverse. Kafka’s own transactions cannot cover the database, and the database’s transactions cannot cover Kafka.

Option 1

Outbox pattern

Write the business change and an event row into the database in one local transaction. A separate process reads the event rows and publishes them to Kafka, marking them as sent.

Option 2

Change data capture

Let a tool read the database’s own change log and publish each committed change to Kafka. The database is the single source of truth.

Option 3

Saga with compensation

Break the business action into steps, each with an undo action. If a later step fails, run the undo actions of earlier steps.

Option 4

Idempotent consumers

Accept that messages may repeat, and make downstream processing safe to repeat through unique identifiers and deduplication.

Option 5

External two-phase commit

Let an outside transaction manager coordinate the database and Kafka as two participants, using prepare and commit for both.

Kafka as a participant in an outside two-phase commit

The classic way to combine systems is to let an external transaction manager act as the coordinator and treat each system as a participant. For that to work, Kafka would need to support a prepared state that survives producer failure and that can be resolved later by the external coordinator. A Kafka improvement proposal was written to add exactly this capability, allowing a producer to keep a prepared transaction alive across restarts and to be told later whether to commit or abort. The work has been progressing across recent releases, so anyone considering it should read the current release notes to see which parts are available in their version.

Simple Analogy

Two banks in different countries agree to transfer funds through a trusted third-party clearing house. Each bank promises to hold the money ready, and only when both promises are in does the clearing house tell them both to complete. Kafka joining as one of the banks needs to be able to hold its promise until the clearing house speaks.

ApproachConsistency strengthComplexityTypical fit
Outbox with relayReliable delivery, at least once, deduplicate downstreamModerateMost service-to-Kafka publishing
Change data captureFollows the database exactlyModerateDatabase-centric systems
SagaEventual, with compensationsHigh, needs undo logicLong business processes across services
External two-phase commitAtomic across participantsHigh, needs a transaction managerStrict atomicity between database and Kafka
Kafka-only transactionAtomic across Kafka topicsLow to moderatePure streaming pipelines
!
Atomicity across systems is costly

Every extra participant lengthens transactions and increases the impact of failures. Prefer patterns such as the outbox, which give reliable outcomes with local transactions only, unless strict cross-system atomicity is a true requirement.

23The Cost of Transactions: Performance Thinking

Transactions are not free, but their cost is often smaller than people fear when used well.

Where the extra work comes from

Cost 1

Coordinator round trips

Registering partitions and requesting the commit or abort involve extra requests to the coordinator.

Cost 2

Transaction log writes

Each state change is a replicated write to the transaction log, adding a small amount of latency and load.

Cost 3

Marker writes

One control record per partition per transaction is written and replicated, adding a little to storage and network use.

Cost 4

Consumer delay

Read-committed consumers wait for transactions to finish before seeing data, adding end-to-end latency equal to the transaction duration.

The fixed cost is per transaction, not per record

The extra requests and writes depend mostly on the number of transactions and the number of partitions involved, not on how many records are inside. This is the key to performance planning. A transaction containing thousands of records pays roughly the same overhead as one containing ten. Small transactions therefore waste a lot of effort, while very large ones increase latency and memory use and lengthen the time the last stable offset is held back.

Simple Analogy

Sending a parcel has a fixed cost for paperwork and a small extra cost per kilogram. Sending one letter at a time by registered post is wasteful. Waiting a year to fill a huge container makes recipients wait too long. There is a sensible middle size.

Larger transactions

  • Lower overhead per record
  • Higher throughput
  • Fewer marker writes overall

Smaller transactions

  • Lower end-to-end latency for readers
  • Less work to redo after a failure
  • Shorter delay of the last stable offset

Tuning the commit interval

In stream processing, the commit interval sets the transaction size in time. Shorter intervals serve low-latency needs, and longer intervals raise throughput. Measure and choose based on the service’s targets.

Limiting partitions per transaction

Every partition touched in a transaction needs its own registration and marker. Designs that touch fewer partitions per transaction are cheaper.

Sizing the coordinator layer

Very large numbers of transactional identifiers spread load across transaction log partitions. Watch the load on brokers that lead busy ones.

i
Measure before deciding

Compare throughput and latency of your pipeline with and without transactions under realistic load. The difference is usually modest for moderately sized transactions, and the correctness gain often outweighs it.

24Key Settings Explained in Words

You do not need every setting memorized, but you must understand what each family controls.

On the producer

Setting

Transactional identifier

The stable name of the worker. Its presence turns on transactions. It must be unique per logical worker and stable across restarts.

Setting

Idempotence

Required for transactions and enabled automatically when a transactional identifier is set, together with strict acknowledgement settings.

Setting

Transaction timeout

How long a transaction may stay open before the coordinator aborts it.

Setting

Acknowledgements

Must require all in-sync replicas to confirm, so acknowledged records survive leader failure.

On the consumer

Setting

Isolation level

Read committed to see only settled transactional data. The default reads everything.

Setting

Automatic offset commit

Turned off in consume-transform-produce loops, because offsets are committed through the transaction instead.

On the broker

Setting familyWhat it controlsGuidance
Transaction log partition countHow many partitions spread coordinator loadSet once at cluster creation; changing later is problematic
Transaction log replication factorHow many copies of the decision log existKeep at three or more for production
Transaction log minimum in-sync replicasHow many copies must confirm a writeKeep strict, commonly two
Maximum transaction timeoutUpper limit producers may requestKeep modest so failures do not freeze readers long
Transactional identifier expirationHow long idle identifiers are rememberedDefault of days is usually fine
Abort timed-out transactions intervalHow often the coordinator scans for expired transactionsLeave at default unless measured otherwise
!
Settings that are hard to change later

The number of transaction log partitions determines the mapping from identifiers to coordinators. Altering it on a live cluster can change which broker coordinates an identifier and is unsafe. Choose it carefully when creating the cluster.

i
Small clusters and defaults

Development clusters with a single broker need the transaction log replication settings relaxed to start at all. Never carry those relaxed settings into production.

25Security Considerations

The transaction machinery introduces its own resources that must be protected.

Transactional identifier as a protected resource

When access control is enabled, the transactional identifier is treated as a resource in its own right. A producer must be allowed to write using that specific identifier, in addition to being allowed to write to the topics involved. This prevents one application from hijacking another’s identifier and thereby fencing it out or aborting its transactions.

Permission

Write on the transactional identifier

Lets a principal use that identifier to begin and end transactions.

Permission

Describe on the transactional identifier

Lets a principal find the coordinator and query transaction information.

Permission

Write on output topics

Required to write the transaction’s records, as with any producer.

Permission

Read on the consumer group

Required to attach consumer offsets to a transaction.

Simple Analogy

A hotel gives each guest a keycard for their own room and a separate authority for the reservation desk. Having access to the corridor does not mean you can change or cancel someone else’s booking.

Protecting the wider picture

Name identifiers by owner and job

Use predictable prefixes so access rules can grant an application rights to its own family of identifiers, without giving it rights to others.

Encrypt traffic and restrict brokers

Transaction requests carry identifiers and metadata that should not be exposed. Use encrypted connections and network controls.

Restrict who can run administrative transaction tools

The ability to abort transactions is powerful. Limit it to a small set of trusted operators and log its use.

Protect the transaction log

The internal transaction log should not be readable or writable by ordinary applications. Restrict access to administrators and brokers.

!
Fencing as an attack

An unauthorized client that guesses another application’s identifier could repeatedly fence it and stop its processing. Access control on identifiers turns this into a rejected request instead of an outage.

26Monitoring, Logging and Metrics

Healthy transactions are quiet. Good monitoring lets you notice trouble before consumers do.

Question 1

How long do transactions last?

Producer-side timing metrics for beginning, flushing and committing reveal the duration of each step.

Question 2

Are transactions being aborted?

A rising abort rate signals application errors, timeouts or fencing, and deserves investigation.

Question 3

Is the last stable offset falling behind?

Broker metrics for the gap between the high watermark and the last stable offset reveal open transactions holding readers back.

Question 4

Is read-committed lag growing?

Consumer lag measured against the last stable offset shows how far behind read-committed consumers are.

Question 5

Are we seeing fencing errors?

Producer fenced errors indicate duplicate identifiers, zombies or overly aggressive restarts.

Question 6

Is the transaction log healthy?

Replication and availability of the internal transaction log partitions must be watched like any critical topic.

SignalWhere to see itHealthy signWarning sign
Transaction commit timeProducer metricsStable and lowGrowing or spiky
Abort rateApplication and producer metricsNear zeroSustained increase
Last stable offset lagBroker partition metricsNear zeroSteadily growing
Fenced producer errorsApplication logsOnly during planned restartsRepeated without restarts
Coordinator load and errorsBroker metrics and logsBalanced across brokersHot broker, frequent retriable errors
Transaction log under-replicated partitionsBroker replication metricsZeroAny sustained count
i
Alert suggestions

Alert on last stable offset lag beyond a few multiples of your transaction timeout, on abort rates above your baseline, on any under-replicated transaction log partition, and on repeated fenced errors for one identifier.

Logs worth reading

Broker logs record coordinator loading after a failover, timed-out transactions being aborted, and marker delivery problems. Application logs record fenced errors, abort decisions and retries. When investigating, line up the timestamps of the producer’s log, the coordinator’s log and the affected partition leader’s log to reconstruct the sequence of a transaction.

27Inspecting Transactions With Built-in Tools

Kafka includes tools that let you see transactions rather than guess about them.

Tool

Transactions tool

Lists active transactional identifiers, describes one identifier’s state, finds hanging transactions on a partition and can abort a hanging one.

Tool

Log dump tool

Reads partition segments and can show control batches, so you can see commit and abort markers in place.

Tool

Consumer group tool

Shows lag and offsets for groups, useful for comparing read-committed lag with expectations.

Tool

Topic description tool

Confirms replication settings for the internal transaction log topic.

Practical uses

Diagnosing a stuck consumer

If a read-committed consumer stops advancing while data flows, use the transactions tool to look for a long-open or hanging transaction on that partition.

Confirming which coordinator owns an identifier

Describe the identifier to see its state, epoch and coordinator, useful when tracing fencing errors.

Viewing markers in a partition

Dump a segment and look for control batches to verify that commit or abort markers were written where expected.

!
Read first, change later

Start with read-only inspection. Abort operations should be a last resort, taken only after confirming that a transaction is genuinely abandoned and recording what was done.

28Advantages and Trade-offs

Every guarantee has a price. Seeing both sides clearly helps you decide when transactions are worth it.

Advantages

  • Atomic writes across many topics and partitions
  • Input positions and outputs committed together, removing lost or duplicate work
  • Zombie instances are fenced automatically by epochs
  • Replicated coordinator state, so failover does not block indefinitely
  • Read-committed consumers see only settled results
  • Built on ordinary Kafka parts, so there is no separate system to run

Disadvantages / Trade-offs

  • Extra latency for readers, at least the length of the transaction
  • Additional load from coordinator requests, log writes and markers
  • A single long transaction can hold back all read-committed consumers of a partition
  • Guarantee stops at Kafka’s boundary, leaving external side effects unprotected
  • Care needed with identifiers, timeouts and error handling in application code
  • Operational tools and knowledge required to diagnose hanging transactions

When to use transactions

SituationRecommendationReason
Financial or accounting streams where duplicates are costlyUse transactionsCorrectness outweighs latency cost
Counting and aggregation pipelinesUse exactly-once processingDuplicates directly distort results
Simple log shipping tolerant of rare duplicatesIdempotent producer may be enoughLower overhead, simpler operation
Ultra-low-latency fire-and-forget telemetryUsually avoid transactionsReaders cannot afford to wait for commits
Single-partition writes with no offset couplingIdempotence sufficesNo multi-partition atomicity needed
“Use transactions where a duplicate or a half-finished result would cost more than a few milliseconds of waiting.”

29Design Patterns and Best Practices

These habits keep transactional pipelines calm and predictable.

Practice 1

One identifier per logical worker

Derive identifiers from stable facts such as application name and input partition, so a replacement always reuses its predecessor’s identifier.

Practice 2

Keep transactions short and steady

Commit on a regular short interval so the last stable offset advances smoothly and failures cost little rework.

Practice 3

Use read committed downstream

Any consumer that must respect transaction outcomes should be configured to read committed data.

Practice 4

Treat fenced errors as fatal

On a fenced error, stop the worker, release resources and let the newer instance continue. Do not attempt to carry on.

Practice 5

Make outside actions idempotent

Attach unique identifiers to external calls so repeats after aborts do no harm.

Practice 6

Protect the transaction log

Keep its replication high, minimum in-sync replicas strict and its brokers healthy, since it stores every decision.

Practice 7

Prefer higher-level frameworks

Use stream processing exactly-once modes rather than assembling the consume, transform and produce loop by hand.

Practice 8

Rehearse failures

Kill producers, coordinators and partition leaders in a test environment, and confirm that outputs remain correct and consumers recover.

i
Write a runbook

Document, for your team, how to recognize a hanging transaction, how to confirm it is abandoned, and how to abort it. A calm runbook written in advance beats improvisation during an incident.

A checklist before going live

1

Confirm durable settings

Verify the transaction log has the intended replication and minimum in-sync replicas.

2

Confirm identifier scheme

Check that every worker has a unique, stable identifier and that access rules are in place.

3

Confirm consumer isolation

Verify downstream consumers read committed data where needed.

4

Confirm monitoring

Check dashboards and alerts for commit time, aborts, lag and fencing.

5

Run failure drills

Test crashes at several points and verify that no duplicates or partial results appear.

30Anti-patterns to Avoid

Each of these looks harmless until a failure makes it expensive.

ANTI-PATTERN-01 Avoid
Problem

Generating a random transactional identifier every time an application starts.

Why It’s Harmful

The cluster cannot link the new instance to its predecessor, so it cannot fence zombies or clean up the predecessor’s open transaction. The old transaction hangs until timeout and duplicate processing becomes possible.

Correct Approach

Use a stable identifier per logical worker so successive instances replace each other cleanly.

ANTI-PATTERN-02 Avoid
Problem

Sharing one transactional identifier among several concurrently running workers.

Why It’s Harmful

Each new initialization fences the others. Workers keep knocking each other out and the application makes little or no progress.

Correct Approach

Give every concurrent worker its own identifier.

ANTI-PATTERN-03 Avoid
Problem

Leaving transactions open while waiting for slow external calls or user input.

Why It’s Harmful

An open transaction holds back the last stable offset for everyone reading that partition, and risks hitting the timeout, which forces an abort and repeated work.

Correct Approach

Keep transactions to the shortest possible span of Kafka work, and perform slow external steps outside them.

ANTI-PATTERN-04 Avoid
Problem

Reading transactional output with the default read-uncommitted setting.

Why It’s Harmful

Consumers see records from open and aborted transactions, defeating the entire purpose of the transactions.

Correct Approach

Set consumers that depend on transaction outcomes to read committed data.

ANTI-PATTERN-05 Avoid
Problem

Weakening replication settings of the transaction log to save resources.

Why It’s Harmful

Losing a recorded decision can make the cluster forget that a transaction was committed, breaking atomicity and correctness.

Correct Approach

Keep the transaction log strongly replicated with strict minimum in-sync replicas in production.

ANTI-PATTERN-06 Avoid
Problem

Ignoring producer fenced errors by retrying the same work.

Why It’s Harmful

A fenced producer is a stale instance. Continuing wastes effort and risks confusion, since a newer instance already owns the work.

Correct Approach

Treat the error as fatal and shut the stale worker down cleanly.

ANTI-PATTERN-07 Avoid
Problem

Claiming exactly-once behavior for a pipeline that calls external services without safeguards.

Why It’s Harmful

Aborted and repeated transactions will repeat external calls, causing duplicate payments or messages that Kafka cannot undo.

Correct Approach

Make external actions idempotent or use patterns such as the outbox to move them outside the transaction.

31Common Myths About Kafka Transactions

A few persistent misunderstandings cause a surprising number of production problems.

Myth: Transactions make Kafka a database

Kafka transactions provide atomic publication across partitions. They do not provide general database features such as queries, locks or isolation between arbitrary readers and writers.

Myth: Exactly-once means no message is ever delivered twice

Messages may still be stored or delivered more than once at lower layers. The guarantee is about the effect on the results, which appear exactly once to read-committed consumers.

Myth: Aborted data is deleted immediately

Aborted records stay in the log until normal retention removes them. They are merely hidden from read-committed consumers.

Myth: Enabling idempotence is the same as using transactions

Idempotence removes duplicates in one partition within one session. Transactions add atomicity across partitions and stable identity across restarts.

Myth: The coordinator is a single point of failure

The coordinator’s state is stored in a replicated log and another broker can take over quickly, recovering unfinished decisions.

Myth: Consumers must do something special to participate

Consumers only need to choose the read-committed isolation level. The client library handles markers and filtering automatically.

32Real-World and Industry Perspectives

Transactions appear wherever duplicates or partial results are costly.

Payments and ledgers

Streams that transform payment events into ledger entries use transactions so that a debit event and its matching credit event appear together or not at all.

E-commerce order pipelines

One order can produce reservation, shipping and notification events. Publishing them atomically keeps downstream services consistent.

Real-time analytics

Counters, sums and windows built from event streams stay accurate across failures when processing is exactly-once.

Data replication and migration

Copying data between clusters or systems with transactional batches avoids duplicated or missing records during outages.

Fraud detection and risk scoring

Feature and score updates derived from event streams should not double count, so pipelines often rely on exactly-once behavior.

Managed streaming platforms

Cloud providers and managed services run the transaction machinery on their clusters, so their users benefit from the guarantees without operating the coordinator layer themselves.

All or none
publication across partitions
One owner
per identifier, enforced by epochs
Replicated
decision log survives failures

33Delivery Guarantees Compared

Transactions are one point on a spectrum. Seeing the whole spectrum shows when each option fits.

GuaranteeWhat can happen after a failureHow it is achievedCost
At most onceRecords may be lost, never repeatedSend without retries and commit input positions before processingLowest, but data can vanish
At least onceRecords are never lost, but may be repeatedRetry on failure and commit input positions after processingLow, duplicates must be tolerated downstream
Idempotent at least onceRetries within one partition and one session do not duplicateIdempotent producer with sequence numbersLow, no multi-partition atomicity
Exactly once inside KafkaOutputs and input positions appear once, atomicallyTransactions, fencing and read-committed consumersModerate, added latency for readers
Effectively once with outside systemsRepeated attempts have no additional effectIdempotent external actions, outbox or deduplicationDepends on the external system
Simple Analogy

Sending an important letter. At most once is dropping it in the box and hoping. At least once is sending it again if you are unsure, so the friend may get two copies. Exactly once is using a registered service with a tracking number that ensures one copy, delivered and signed for.

Choosing the right level

When duplicates are harmless

Metrics, logs and cache invalidations can often tolerate repeats. At least once with idempotence is a sensible choice.

When duplicates are costly

Money movements, inventory counts and billing events should use transactions or strongly idempotent processing.

When data loss is the bigger fear

Never choose at most once for data you cannot recreate. Prefer at least once or stronger.

i
Match the guarantee to the harm

Ask what a duplicate would cost and what a lost record would cost. The answer tells you how much protection to pay for.

34A Worked Example: An Order Processing Pipeline

Let us apply everything to a small, realistic pipeline and watch the guarantees at work.

A shop publishes an order event to a topic named orders. A processing service reads each order, then writes three events: a payment request to one topic, a stock reservation to another and an audit entry to a third. The service runs as three instances, each responsible for a share of the input partitions. The team wants no order to be processed twice and no order to be half processed.

1

Assign identifiers

Each instance uses a transactional identifier derived from the service name and the input partitions it owns, so a replacement instance inherits the same identifier.

2

Poll and begin

The instance reads a batch of orders with automatic offset commits off and begins a transaction.

3

Write three outputs per order

For each order, the payment request, reservation and audit entry are written inside the transaction to their respective partitions.

4

Attach input offsets

The instance adds the input positions of the batch to the transaction.

5

Commit

The coordinator records the decision and writes markers to all output partitions and to the offsets partition.

6

Downstream services read committed

The payment, stock and audit services read with the read-committed setting, so they only ever see events belonging to committed batches.

What happens when an instance crashes halfway

Suppose an instance dies after writing the payment request and reservation for one order, but before committing. The downstream services see nothing, because the transaction is still open. After the timeout, the coordinator aborts it and the three records stay hidden forever. A replacement instance starts with the same transactional identifier, is given a higher epoch, and re-reads the batch from the last committed input position. It processes the same orders again in a fresh transaction, and this time the results are committed. Downstream services see exactly one payment request per order.

sequenceDiagram
    participant I1 as Instance one
    participant TC as Coordinator
    participant D as Downstream services
    participant I2 as Replacement instance
    I1->>TC: Begin and write partial outputs
    I1->>I1: Crash
    D->>D: Sees nothing, transaction still open
    TC->>TC: Timeout reached, abort
    I2->>TC: Initialize with same identifier, epoch raised
    I2->>I2: Re-read from last committed offsets
    I2->>TC: Write outputs and commit
    D->>D: Sees each result exactly once
        
FIG 12 — A crash mid-batch leaves no visible trace
!
Where the boundary lies

If the payment service then calls an outside card processor, that call is beyond Kafka’s guarantee. The payment service must send a unique identifier with each charge so the processor can ignore repeats.

35Testing Transactional Pipelines Before Production

Guarantees that are never tested under failure are only hopes. A modest testing routine turns hope into evidence.

Test 1

Crash the producer at each step

Stop the application after begin, after some writes, after the commit request and after commit, and verify that outputs contain no duplicates or partial results in each case.

Test 2

Restart the coordinator broker

Stop the broker leading the relevant transaction log partition during transactions and confirm they complete or abort cleanly after failover.

Test 3

Pause an instance to create a zombie

Freeze a worker until a replacement starts, then resume it and confirm it is fenced and produces no output.

Test 4

Kill a partition leader mid-commit

Force a leader election on an output partition while markers are being written and confirm all partitions eventually show the outcome.

Test 5

Let a transaction time out

Hold a transaction open past the timeout and verify that it is aborted and that read-committed consumers resume.

Test 6

Verify with a checker

Feed known input and compare the final committed output against an independent count, looking for any missing or repeated results.

Simple Analogy

Fire drills prove that an evacuation plan works. Nobody trusts a plan that has only been written on paper. Crashing your own pipeline on purpose is the fire drill for your transactions.

i
Automate the drills

Keep these failure tests in a repeatable suite and run it before each upgrade of clients, brokers or application logic. Bugs in transactional code tend to appear only under failure, so the suite is where they will be found.

36Frequently Asked Questions

Q1Is Kafka’s transaction protocol exactly the textbook two-phase commit?

Not exactly. It shares the durable decision point and the second phase that spreads the decision, but it has no voting step, the coordinator is replicated, and data is written to partitions before the decision.

Q2What is the point of no return in a Kafka transaction?

The moment the coordinator durably records the prepare-commit or prepare-abort state in the transaction log. After that, the outcome cannot change.

Q3Does the producer wait for markers to be written?

No. Its commit call returns once the decision is durably recorded. The coordinator writes markers to partitions afterwards.

Q4What happens if the coordinator crashes after the decision?

A new coordinator loads the transaction log, finds the transaction in a prepare state and finishes the second phase by sending the markers.

Q5What is a zombie producer and how is it stopped?

It is an older instance that was thought dead but is still running. Each initialization for a transactional identifier raises the epoch, and requests with an older epoch are rejected, which stops the zombie.

Q6Why do read-committed consumers sometimes stall?

They can only read up to the last stable offset, which stops at the earliest open transaction. A long or hanging transaction therefore holds them back until it finishes or times out.

Q7Why do offsets seem to skip numbers?

Commit and abort markers take up offsets in the log but are not delivered to applications, so consumers see gaps where the markers sit.

Q8Are aborted records physically removed?

No. They remain in the log until normal retention removes them, and are simply hidden from read-committed consumers.

Q9Do I need transactions if I already use the idempotent producer?

Only if you need atomicity across several partitions or need to commit consumer offsets together with outputs. Idempotence alone handles retry duplicates within one partition.

Q10Can Kafka transactions include a database write?

Not by themselves. Patterns like the outbox, change data capture, or an external transaction manager are used to coordinate Kafka and a database, and newer proposals aim to let Kafka join an outside two-phase commit.

Q11How is the coordinator for an identifier chosen?

The identifier is hashed to a partition of the transaction log, and the broker leading that partition acts as coordinator. If leadership moves, the coordinator role moves with it.

Q12What should the transaction timeout be?

Longer than your longest normal transaction, but short enough that a failure does not freeze read-committed consumers for long. Measure real transaction durations and choose accordingly.

Q13Why must consumer automatic offset commit be off in a transactional loop?

Because offsets must be committed through the transaction, together with outputs. Automatic commits would record progress independently and break the atomic link.

Q14What is a hanging transaction?

A transaction that remains open on a partition even though the coordinator no longer treats it as active. It holds back the last stable offset and requires administrative action or protocol improvements to clear.

Q15Does using transactions slow my pipeline a lot?

The overhead is mostly fixed per transaction, so moderately sized transactions cost little relative to their content. The main visible effect is added latency for read-committed consumers, roughly the duration of the transaction.

Q16Can I change the number of transaction log partitions later?

It is unsafe on a live cluster because it changes the mapping from identifiers to coordinators. Choose the number carefully when creating the cluster.

Q17Who needs to know about transactions: producers only?

Producers begin and end transactions, but consumers must choose the read-committed level to honor them, and operators must monitor and protect the coordinator layer and the transaction log.

37Summary and Key Takeaways

Kafka’s transaction protocol takes the classic two-phase commit idea and adapts it to a streaming platform. A stable transactional identifier gives each worker a durable identity, epochs fence out stale instances, and a replicated coordinator records every decision in a transaction log. Records are written to partitions during the transaction, the decision to commit or abort is made durable in phase one, and markers are written to every partition in phase two so that read-committed consumers see all of the results or none of them. Combined with transactional offset commits, this yields exactly-once processing inside Kafka, and understanding its boundaries lets you design safe pipelines around the outside world.

Key Takeaways

  • Two phases, one durable decision — the write of prepare-commit or prepare-abort to the replicated transaction log is the point of no return, and the marker phase merely spreads it.
  • No voting, replicated coordinator — partition leaders do not vote, and a new coordinator can always finish an interrupted decision, avoiding the classic blocking problem.
  • Identity and fencing keep one owner — the transactional identifier and rising epoch ensure only one live instance per identifier can make progress.
  • Markers and the last stable offset hide unsettled data — read-committed consumers see only committed results, and a long open transaction can hold them back.
  • Offsets travel inside the transaction — consume, transform and produce becomes atomic when input positions are committed together with outputs.
  • Exactly-once has a boundary — it covers Kafka to Kafka flows, and external side effects need idempotence, outbox patterns or an external coordinator.
  • Operate it deliberately — keep the transaction log strongly replicated, choose sensible timeouts and transaction sizes, secure identifiers, and monitor aborts, lag and fencing.