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.
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
Partial writes
The application crashes after writing to two topics but before writing to the third. Downstream systems see an inconsistent picture.
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.
Lost progress
The application writes results but crashes before recording that it has processed the input, so on restart it processes the input again.
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.
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.
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
Ask
The coordinator asks each participant to prepare.
Promise
Each participant durably records its readiness and replies.
Decide
The coordinator durably records a single, final decision.
Tell
The coordinator instructs every participant to commit or abort.
Confirm
Participants finish and acknowledge, and the coordinator closes the transaction.
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.
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
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.
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.
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.
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.
| Aspect | Classic two-phase commit | Kafka transactions |
|---|---|---|
| Coordinator durability | Coordinator’s own log, usually one machine | Replicated transaction log with failover |
| Participant voting | Yes, each participant votes | No voting, participants apply markers |
| When data is written | During or after prepare | During the transaction, before prepare |
| Decision point | Coordinator records commit or abort | Coordinator records prepare-commit or prepare-abort |
| How readers see results | Locks hide uncommitted data | Markers and read-committed filtering hide uncommitted data |
| Blocking risk | High when the coordinator fails | Lower, recovered by the new coordinator, bounded by timeouts |
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.
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.
Transaction coordinator
A component running inside a broker. It tracks each transaction’s state, decides the outcome and drives the completion.
Transaction log
An internal, replicated topic where the coordinator durably records every state change of every transaction.
Partition leaders
The brokers holding the partitions the transaction writes to. They store records and later store the commit or abort markers.
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
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.
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.
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.
Producer identifier
A number, often called the PID, that the cluster assigns to identify one producer session.
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.
Sequence numbers
A per-partition counter that lets the broker detect duplicates and out-of-order batches.
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
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.
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
Find the coordinator
The producer asks any broker which broker is the transaction coordinator for its transactional identifier.
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.
Raise the epoch
The coordinator increases the epoch, so any older instance that used the same transactional identifier now holds an outdated epoch.
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.
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
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.
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.
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"]
What is stored in the log
Transactional identifier
The key, so newer entries replace older ones for the same worker when the log is compacted.
Producer identifier and epoch
The current identity of the worker, used for fencing.
Transaction timeout
How long the worker is allowed to keep a transaction open.
Current state
Where the transaction is in its life, such as ongoing or preparing to commit.
Involved partitions
The list of topic partitions that the transaction has written to so far.
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.
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.
Empty
No transaction is active for this identifier. The worker is registered and idle.
Ongoing
A transaction is open. Partitions may be added and records written.
Prepare commit
The coordinator has durably decided to commit, but markers have not all been written yet.
Prepare abort
The coordinator has durably decided to abort, but markers have not all been written yet.
Complete commit
All commit markers are written. The transaction is finished and its data is visible.
Complete abort
All abort markers are written. The transaction is finished and its data stays hidden.
Dead
The identifier has expired after a long period of inactivity and its record is being removed.
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
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.
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.
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.
Initialize
At startup the producer finds its coordinator and obtains its identity and epoch, as described earlier. This happens once per producer session.
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.
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.
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.
Request commit
The application asks to commit. The producer flushes any pending records and sends a commit request to the coordinator.
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.
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.
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
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.
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 crash | What the recovered coordinator concludes | What it does next |
|---|---|---|
| Nothing written for this transaction | No transaction is known | Waits for the producer to reinitialize |
| Ongoing state written, no prepare entry | The transaction was not decided | Waits for the producer, or aborts on timeout |
| Prepare commit entry written | The decision was commit | Sends commit markers and completes |
| Prepare abort entry written | The decision was abort | Sends abort markers and completes |
| Complete entry written | Everything is finished | Nothing 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.
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.
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.
Group by broker
The coordinator collects the involved partitions and groups them by the broker currently leading each one.
Send marker requests
One request per broker carries all the markers that broker must write.
Append and replicate
Each leader appends control records and replicates them to its followers.
Collect acknowledgements
The coordinator tracks which partitions have confirmed and retries the rest.
Write the completion state
When all partitions have confirmed, the coordinator records the transaction as complete in the log.
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.
Transactional data batch
Contains user records. Its header carries the producer identifier, epoch and a flag saying it belongs to a transaction.
Commit marker
A control batch declaring that the producer’s transaction has committed. It uses one offset in the log.
Abort marker
A control batch declaring that the producer’s transaction has aborted.
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"]
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.
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.
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"]
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 boundary | Meaning | Who cares |
|---|---|---|
| Log start offset | Oldest record still retained | Retention and consumers starting from the beginning |
| Last stable offset | Everything before it is settled | Read-committed consumers |
| High watermark | Everything before it is replicated to all in-sync replicas | Read-uncommitted consumers |
| Log end offset | Next offset to be written | Leaders and replication |
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.
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.
Poll input
The consumer reads a batch of input records, with automatic offset commits turned off.
Begin the transaction
The producer marks the start of a new transaction.
Write outputs
Transformed results are sent to one or more output topics inside the transaction.
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.
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"]
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
The application aborts
The application catches an error during processing and explicitly asks to abort the transaction.
Timeout
The transaction has been open longer than its allowed time, and the coordinator aborts it on its own.
Fencing
A newer instance with the same transactional identifier initializes, and the coordinator aborts the older instance’s open transaction.
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
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.
| Failure | Outcome for the transaction | Who resolves it |
|---|---|---|
| Producer dies before commit request | Aborted | Coordinator by timeout, or new instance by fencing |
| Producer dies after decision recorded | Completes as decided | Coordinator |
| Coordinator dies before decision | Still open, later committed or aborted | New coordinator with the producer |
| Coordinator dies after decision | Completes as decided | New coordinator |
| Partition leader dies during markers | Completes after retry | Coordinator |
| Zombie producer resurfaces | Its writes are rejected | Epoch fencing |
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.
Become leader
The broker is chosen as leader for a partition of the transaction log.
Read the log
It reads every entry in the partition and builds a table of current transaction states.
Find unfinished decisions
Entries in a prepare state indicate a decision that still needs its markers written.
Resume phase two
For each prepare entry, it sends the corresponding markers to the recorded partitions and finishes the transaction.
Start timers
It starts watching ongoing transactions so that any that exceed their timeout are aborted.
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
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.
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.
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.
Read-committed lag climbs
Lag for read-committed consumers rises, while read-uncommitted consumers of the same partition keep up.
Last stable offset is stuck
The gap between the high watermark and the last stable offset grows and never shrinks.
List open transactions
Use the transactions tool to list and describe transactions, and to search for hanging ones on a suspect partition.
Abort the hanging one
Use the same tool to abort a confirmed hanging transaction, after checking that it is truly abandoned.
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
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.
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.
Make the external call idempotent
Give each action a stable identifier so repeating it has no additional effect.
Record what was done
Track completed actions in a durable store so a retry can skip what has already happened.
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.
Commit interval
Transactions are committed on a regular short interval. A shorter interval reduces latency and duplicate work after failure, but raises transaction overhead.
State store changelogs
Updates to local state are written to changelog topics inside the same transaction, so state and output stay consistent.
Automatic identifiers
The library assigns stable transactional identifiers to processing tasks so that a replacement worker fences its predecessor.
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.
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.
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
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.
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.
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.
Clearer error handling
Errors are classified more precisely into retriable, abortable and fatal, so applications can respond correctly.
| Older behavior | Newer behavior | Benefit |
|---|---|---|
| Partition leader accepts a transactional write on trust | Leader verifies the partition belongs to the open transaction | Stops late writes from corrupting later transactions |
| Epoch changes only at initialization | Epoch changes at each transaction | Stale requests from earlier transactions are fenced |
| Separate partition registration request before the first write | Registration implied by the first write | Lower latency per transaction |
| Some error cases ambiguous | Errors clearly categorized | Correct retry or abort behavior in clients |
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.
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.
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.
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.
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.
Idempotent consumers
Accept that messages may repeat, and make downstream processing safe to repeat through unique identifiers and deduplication.
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.
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.
| Approach | Consistency strength | Complexity | Typical fit |
|---|---|---|---|
| Outbox with relay | Reliable delivery, at least once, deduplicate downstream | Moderate | Most service-to-Kafka publishing |
| Change data capture | Follows the database exactly | Moderate | Database-centric systems |
| Saga | Eventual, with compensations | High, needs undo logic | Long business processes across services |
| External two-phase commit | Atomic across participants | High, needs a transaction manager | Strict atomicity between database and Kafka |
| Kafka-only transaction | Atomic across Kafka topics | Low to moderate | Pure streaming pipelines |
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
Coordinator round trips
Registering partitions and requesting the commit or abort involve extra requests to the coordinator.
Transaction log writes
Each state change is a replicated write to the transaction log, adding a small amount of latency and load.
Marker writes
One control record per partition per transaction is written and replicated, adding a little to storage and network use.
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.
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.
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
Transactional identifier
The stable name of the worker. Its presence turns on transactions. It must be unique per logical worker and stable across restarts.
Idempotence
Required for transactions and enabled automatically when a transactional identifier is set, together with strict acknowledgement settings.
Transaction timeout
How long a transaction may stay open before the coordinator aborts it.
Acknowledgements
Must require all in-sync replicas to confirm, so acknowledged records survive leader failure.
On the consumer
Isolation level
Read committed to see only settled transactional data. The default reads everything.
Automatic offset commit
Turned off in consume-transform-produce loops, because offsets are committed through the transaction instead.
On the broker
| Setting family | What it controls | Guidance |
|---|---|---|
| Transaction log partition count | How many partitions spread coordinator load | Set once at cluster creation; changing later is problematic |
| Transaction log replication factor | How many copies of the decision log exist | Keep at three or more for production |
| Transaction log minimum in-sync replicas | How many copies must confirm a write | Keep strict, commonly two |
| Maximum transaction timeout | Upper limit producers may request | Keep modest so failures do not freeze readers long |
| Transactional identifier expiration | How long idle identifiers are remembered | Default of days is usually fine |
| Abort timed-out transactions interval | How often the coordinator scans for expired transactions | Leave at default unless measured otherwise |
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.
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.
Write on the transactional identifier
Lets a principal use that identifier to begin and end transactions.
Describe on the transactional identifier
Lets a principal find the coordinator and query transaction information.
Write on output topics
Required to write the transaction’s records, as with any producer.
Read on the consumer group
Required to attach consumer offsets to a transaction.
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.
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.
How long do transactions last?
Producer-side timing metrics for beginning, flushing and committing reveal the duration of each step.
Are transactions being aborted?
A rising abort rate signals application errors, timeouts or fencing, and deserves investigation.
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.
Is read-committed lag growing?
Consumer lag measured against the last stable offset shows how far behind read-committed consumers are.
Are we seeing fencing errors?
Producer fenced errors indicate duplicate identifiers, zombies or overly aggressive restarts.
Is the transaction log healthy?
Replication and availability of the internal transaction log partitions must be watched like any critical topic.
| Signal | Where to see it | Healthy sign | Warning sign |
|---|---|---|---|
| Transaction commit time | Producer metrics | Stable and low | Growing or spiky |
| Abort rate | Application and producer metrics | Near zero | Sustained increase |
| Last stable offset lag | Broker partition metrics | Near zero | Steadily growing |
| Fenced producer errors | Application logs | Only during planned restarts | Repeated without restarts |
| Coordinator load and errors | Broker metrics and logs | Balanced across brokers | Hot broker, frequent retriable errors |
| Transaction log under-replicated partitions | Broker replication metrics | Zero | Any sustained count |
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.
Transactions tool
Lists active transactional identifiers, describes one identifier’s state, finds hanging transactions on a partition and can abort a hanging one.
Log dump tool
Reads partition segments and can show control batches, so you can see commit and abort markers in place.
Consumer group tool
Shows lag and offsets for groups, useful for comparing read-committed lag with expectations.
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.
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
| Situation | Recommendation | Reason |
|---|---|---|
| Financial or accounting streams where duplicates are costly | Use transactions | Correctness outweighs latency cost |
| Counting and aggregation pipelines | Use exactly-once processing | Duplicates directly distort results |
| Simple log shipping tolerant of rare duplicates | Idempotent producer may be enough | Lower overhead, simpler operation |
| Ultra-low-latency fire-and-forget telemetry | Usually avoid transactions | Readers cannot afford to wait for commits |
| Single-partition writes with no offset coupling | Idempotence suffices | No multi-partition atomicity needed |
29Design Patterns and Best Practices
These habits keep transactional pipelines calm and predictable.
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.
Keep transactions short and steady
Commit on a regular short interval so the last stable offset advances smoothly and failures cost little rework.
Use read committed downstream
Any consumer that must respect transaction outcomes should be configured to read committed data.
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.
Make outside actions idempotent
Attach unique identifiers to external calls so repeats after aborts do no harm.
Protect the transaction log
Keep its replication high, minimum in-sync replicas strict and its brokers healthy, since it stores every decision.
Prefer higher-level frameworks
Use stream processing exactly-once modes rather than assembling the consume, transform and produce loop by hand.
Rehearse failures
Kill producers, coordinators and partition leaders in a test environment, and confirm that outputs remain correct and consumers recover.
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
Confirm durable settings
Verify the transaction log has the intended replication and minimum in-sync replicas.
Confirm identifier scheme
Check that every worker has a unique, stable identifier and that access rules are in place.
Confirm consumer isolation
Verify downstream consumers read committed data where needed.
Confirm monitoring
Check dashboards and alerts for commit time, aborts, lag and fencing.
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.
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.
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.
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.
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.
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.
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.
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.
33Delivery Guarantees Compared
Transactions are one point on a spectrum. Seeing the whole spectrum shows when each option fits.
| Guarantee | What can happen after a failure | How it is achieved | Cost |
|---|---|---|---|
| At most once | Records may be lost, never repeated | Send without retries and commit input positions before processing | Lowest, but data can vanish |
| At least once | Records are never lost, but may be repeated | Retry on failure and commit input positions after processing | Low, duplicates must be tolerated downstream |
| Idempotent at least once | Retries within one partition and one session do not duplicate | Idempotent producer with sequence numbers | Low, no multi-partition atomicity |
| Exactly once inside Kafka | Outputs and input positions appear once, atomically | Transactions, fencing and read-committed consumers | Moderate, added latency for readers |
| Effectively once with outside systems | Repeated attempts have no additional effect | Idempotent external actions, outbox or deduplication | Depends on the external system |
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.
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.
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.
Poll and begin
The instance reads a batch of orders with automatic offset commits off and begins a transaction.
Write three outputs per order
For each order, the payment request, reservation and audit entry are written inside the transaction to their respective partitions.
Attach input offsets
The instance adds the input positions of the batch to the transaction.
Commit
The coordinator records the decision and writes markers to all output partitions and to the offsets partition.
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
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.
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.
Restart the coordinator broker
Stop the broker leading the relevant transaction log partition during transactions and confirm they complete or abort cleanly after failover.
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.
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.
Let a transaction time out
Hold a transaction open past the timeout and verify that it is aborted and that read-committed consumers resume.
Verify with a checker
Feed known input and compare the final committed output against an independent count, looking for any missing or repeated results.
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.
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
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.
The moment the coordinator durably records the prepare-commit or prepare-abort state in the transaction log. After that, the outcome cannot change.
No. Its commit call returns once the decision is durably recorded. The coordinator writes markers to partitions afterwards.
A new coordinator loads the transaction log, finds the transaction in a prepare state and finishes the second phase by sending the markers.
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.
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.
Commit and abort markers take up offsets in the log but are not delivered to applications, so consumers see gaps where the markers sit.
No. They remain in the log until normal retention removes them, and are simply hidden from read-committed consumers.
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.
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.
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.
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.
Because offsets must be committed through the transaction, together with outputs. Automatic commits would record progress independently and break the atomic link.
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.
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.
It is unsafe on a live cluster because it changes the mapping from identifiers to coordinators. Choose the number carefully when creating the cluster.
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.