The Transaction Coordinator in Kafka
How Kafka makes a group of writes succeed together or fail together, how one broker acts as the careful referee, and how the whole system avoids duplicates, half-finished work, and confused old programs. Explained from zero, with everyday pictures, clear diagrams, and real-world lessons.
Imagine you are paying for a school trip. Money must leave your account and arrive in the school’s account. If the money leaves but never arrives, you are angry. If it arrives twice, the school is confused. The only acceptable results are “both things happened” or “neither thing happened.” Banks solved this problem long ago with the idea of a transaction, which means a group of actions that must succeed or fail as one unit.
Apache Kafka faces the same problem when a program reads messages, does some work, and writes new messages to several places. If the program crashes in the middle, some outputs may exist and others may not. Worse, when the program restarts, it may repeat work and write duplicates. Kafka’s answer is a feature called transactions, and the special helper that keeps them honest is the transaction coordinator.
In this guide you will learn what the transaction coordinator is, where it lives, how it remembers things, how it commits or cancels a group of writes, what it does when a broker or a client crashes, and how to run it well in production. You do not need any earlier knowledge of transactions, exactly-once processing, or Kafka internals. We begin with the very basics and build up slowly.
Why Transactions Matter: The Big Picture
Before any Kafka words, let us understand the simple problem that transactions solve.
Suppose a small program reads one message that says “customer Asha bought a phone.” The program must do three things: write a message to an “orders-confirmed” topic, write a message to a “stock-reduced” topic, and remember that it has finished reading that input message. If the program crashes after the first write but before the other two, the world is left in a strange state. Asha’s order is confirmed but the stock was never reduced, and after a restart, the program will read the same input message again and confirm it a second time.
Think of a teacher handing back a set of exam papers. The teacher writes marks in the register, stamps the papers, and notes in a diary that this batch is done. If the teacher is called away halfway, the register may show marks that the papers do not, and the diary says nothing. A transaction is a rule that says: “Either everything for this batch is finished and recorded, or it is treated as if the batch never started.”
What a transaction promises
A Kafka transaction promises atomicity. That word means “all or nothing.” Either all the messages written inside the transaction become visible to readers, or none of them do. This holds even when the writes go to many different topics and partitions living on many different brokers, and even when the program or a broker crashes in the middle.
Who keeps the promise?
A single producer program cannot keep this promise alone, because it might crash at the worst moment. Something else must remember what the producer started, decide the final outcome, and finish the job even if the producer is gone. That “something else” is the transaction coordinator. It is not a separate machine. It is a role played by a normal Kafka broker.
The producer asks for the transaction, the coordinator keeps the official record, and the partitions hold the messages. Three actors, one clear job each.
Kafka Basics You Need First
A short tour of the Kafka words we will use again and again.
Broker
A Kafka server that stores messages on disk and serves producers and consumers.
Topic
A named stream of messages, such as “orders” or “payments”.
Partition
A slice of a topic. Each partition is an ordered list of messages with numbered positions called offsets.
Replica
A copy of a partition kept on another broker so a failure does not lose data.
Partition leader
The one replica that handles all reads and writes for a partition. Others copy from it.
Consumer group
A team of consumers that share the work of reading a topic and remember their progress.
Producers are programs that write messages. Consumers are programs that read them. A message is only added to the end of a partition, and it is never changed afterwards. Consumers remember how far they have read using a number called an offset, which Kafka stores in a special internal topic. This is important later, because committing that number is part of what a transaction can do.
A partition is like a long roll of receipt paper that only grows at one end. Each receipt has a number printed on it. A consumer keeps a bookmark at the last receipt it has read. If the bookmark is lost, the reader may read some receipts again or skip some.
Internal topics
Kafka keeps some of its own working notes in topics too. They usually have names that start with two underscores. One holds consumer bookmarks. Another holds the transaction coordinator’s notes, which we will study closely in this guide. Using ordinary Kafka topics for its own memory is a favorite Kafka habit: it gets copying, durability, and recovery for free.
Delivery Guarantees: At-Most-Once, At-Least-Once, Exactly-Once
Three promises that a messaging system can make, and why the third is the hardest.
| Guarantee | Plain meaning | What can go wrong |
|---|---|---|
| At most once | A message is delivered zero or one time | Messages can be lost, but never repeated |
| At least once | A message is delivered one or more times | Nothing is lost, but duplicates can appear |
| Exactly once | Each message has its effect one time only | Hardest to build, and only true inside a defined boundary |
Imagine posting an important letter. At most once means you post it and never check, so it might vanish. At least once means you keep posting copies until you hear back, so the receiver may get several. Exactly once means the receiver gets one letter and knows it is the only one, even if you posted copies by mistake.
Why duplicates happen
A producer sends a message and waits for the broker’s reply. If the reply is lost in the network, the producer cannot tell whether the message arrived. The safe thing is to send it again. If the first message had in fact arrived, the partition now contains two copies. This is the classic origin of duplicates, and it happens even in a perfectly healthy system when the network hiccups.
Where “exactly once” is real
Kafka’s exactly-once feature is real inside Kafka: reading from Kafka topics, processing, and writing to Kafka topics. It cannot magically make an outside system, such as an email service or a database, act exactly once. For outside systems you need extra techniques, such as making the outside action harmless to repeat. We come back to this important limit later.
“Exactly once” is a promise about a boundary, not about the whole universe. Always ask: exactly once where? Inside Kafka topics, yes. Inside your bank’s database, only if you design it that way.
The Idempotent Producer: The Building Block
Transactions stand on the shoulders of a simpler idea that removes duplicates from a single partition.
Idempotent is a mathematical word that means “doing it twice has the same effect as doing it once.” Pressing an elevator button twice does not call two elevators. An idempotent producer works the same way: even if it sends the same message twice, the partition keeps only one copy.
The producer gets an identity
When it starts, it asks the cluster for a producer ID, a unique number, and an epoch, a version counter that starts small and can only go up.
Every message gets a sequence number
For each partition, the producer numbers its messages 0, 1, 2, and so on, like page numbers.
The broker checks the numbers
The partition leader remembers the latest sequence number from each producer. If a message arrives with an old number, it is a duplicate and is quietly ignored. If a number is skipped, the broker reports an error, because something was lost.
Retries become safe
The producer can now retry freely after a lost reply, and no duplicates result.
A librarian who receives numbered pages from a courier checks each page number against the last one received. If page 7 arrives twice, the second copy goes in the bin. If page 9 arrives before page 8, she raises an alarm.
Where idempotence stops
Idempotence protects a producer only for one partition and only as long as the producer keeps the same identity. If the producer crashes and restarts, it usually gets a brand new producer ID, so the broker cannot recognize it as the same producer. And idempotence does nothing for a group of writes that must succeed together across many partitions. To fix these gaps, Kafka added the transaction coordinator, which gives a producer a stable identity that survives restarts and makes group writes atomic.
What Is a Transaction in Kafka?
A precise but friendly definition, with a picture of the most common use.
A Kafka transaction is a bundle of writes, possibly to several topics and partitions, that becomes visible to readers all at once or not at all. The producer opens the bundle, adds writes, and then either commits (makes everything visible) or aborts (makes everything invisible). Additionally, the bundle can include consumer bookmark updates, so that “I have finished reading up to here” is saved atomically with the output messages.
The classic pattern: consume, transform, produce
Consume
Read a batch of messages from an input topic.
Transform
Do your business work: check, enrich, calculate.
Produce
Write results to one or more output topics inside a transaction.
Save the bookmark
Include the input offsets in the same transaction, so progress and results are saved together.
Commit
End the transaction. Results and bookmark become real at the same instant.
If the program crashes anywhere before the commit, the outputs stay invisible and the bookmark stays where it was. After restart, the program reads the same input again and redoes the work, but because the earlier attempt was never committed, no visible duplicates exist. This is the heart of Kafka’s exactly-once processing.
It is like a cashier who prepares a receipt, packs the goods, and marks the order as done, all placed in a sealed tray. The tray is only slid across the counter when everything inside is ready. If the cashier faints halfway, the tray never crosses the counter, and a colleague starts the order from scratch.
Transactions also help simple producers that are not reading anything. A producer may write a small group of related events to different topics, and use a transaction only to make them appear together.
Meet the Transaction Coordinator
The referee of every Kafka transaction.
The transaction coordinator is a component that runs inside every Kafka broker. For any given transaction, exactly one broker plays the coordinator. Its job is to remember which transactions are open, which partitions they touch, and what the final decision is. It makes sure that the decision is written down safely, and then it makes sure the decision is carried out on every partition involved.
Gives out identities
Hands each transactional producer a producer ID and an epoch, and keeps track of them.
Remembers the transaction
Records which partitions a transaction has touched and its current state.
Decides commit or abort
Writes the decision to its durable log before telling anyone.
Delivers the decision
Sends special markers to every partition so readers know the outcome.
Fences old producers
Uses the epoch number to lock out an older copy of a producer that should no longer act.
Aborts stuck transactions
Cancels transactions that stay open too long, so they do not block readers.
Think of a wedding registrar. The couple (the producer) arrives, the registrar records who is getting married and the witnesses (the partitions), writes the official decision in the register, and then sends the certificate to all the offices that need it. If the couple leaves the room, the register still says what was decided.
Which broker is the coordinator?
Any broker could be a coordinator, but for a given transactional ID only one broker is. That ID is a name chosen by the application, such as “payment-processor-7”. Kafka turns the name into a number, and the number decides which partition of the internal transaction log is responsible for it. The broker that currently leads that partition of the log is that producer’s coordinator. This is a neat trick: leadership of a log partition doubles as the coordinator election.
The Transaction Log: The Coordinator’s Notebook
A coordinator that forgets is useless, so its memory lives in a replicated Kafka topic.
The coordinator writes every important change of every transaction into an internal topic called the transaction state log. Its name is __transaction_state. By default it has fifty partitions and each partition is stored on three brokers, with at least two copies required to be in sync before a write is confirmed. The topic is compacted, which means Kafka eventually keeps only the newest record for each transactional ID, like a notebook where old entries for the same person are crossed out.
A name becomes a number
The transactional ID is put through a fixed mathematical function that produces a number. That number, divided by the count of partitions, leaves a remainder that picks one partition of the log.
The partition leader is the coordinator
Whichever broker currently leads that partition acts as the coordinator for that ID.
State is written before it is acted upon
The coordinator saves each state change in the log and waits for the copies to be safe, then continues.
Recovery is just a re-read
If the leader fails, a new leader is chosen from the copies, and it reads the log to rebuild the coordinator’s memory.
Fifty numbered filing cabinets hold the notes, and each person’s file goes into the cabinet chosen by a rule based on their name. Each cabinet has a keeper, plus two spare copies stored in other buildings. If a keeper is absent, one of the spare-copy holders takes over and simply opens the cabinet.
Why the number of partitions is fixed
Because the mapping from name to partition depends on the partition count, the count must not change after the cluster begins using transactions. If it changed, names would suddenly point to different cabinets and the coordinators would lose track of open transactions. This is why the count is set once, at the beginning, and treated as permanent.
Do not try to change the number of partitions of the transaction log on a cluster that has been using transactions. Choose the value carefully at the start. The default is sensible for most clusters.
Architecture and Components
The whole picture: who talks to whom, and why.
flowchart TB
P["Transactional Producer"]
subgraph BR["Kafka Broker Fleet"]
TC["Broker B - Transaction Coordinator"]
TL[("Transaction log partition - led by Broker B")]
L1["Broker A - leader of orders partition 0"]
L2["Broker C - leader of stock partition 3"]
GC["Broker D - group coordinator for offsets"]
end
CON["Read-committed Consumer"]
P -->|"1 find coordinator and get identity"| TC
P -->|"2 register partitions"| TC
TC -->|"saves state"| TL
P -->|"3 send messages"| L1
P -->|"3 send messages"| L2
P -->|"4 send consumer offsets"| GC
P -->|"5 commit or abort"| TC
TC -->|"6 write commit or abort markers"| L1
TC -->|"6 write commit or abort markers"| L2
TC -->|"6 write markers"| GC
L1 -->|"only committed data"| CON
L2 -->|"only committed data"| CON
Read the figure from the left. The producer first talks to the coordinator to get an identity and to register the partitions it plans to write to. It then sends its messages directly to the partition leaders, without going through the coordinator, so the coordinator is not a bottleneck for data. When the producer is ready, it tells the coordinator to commit or abort. The coordinator writes the decision to its log and then sends small markers to every partition involved. Consumers that ask for committed data only see messages after they are marked committed.
The components in plain words
Transactional producer
The client program. It has a stable name, the transactional ID, and calls a small set of transaction commands.
Transaction coordinator
The broker-side referee described in this guide.
Transaction log
The compacted internal topic that stores every transaction’s state durably.
Partition leaders
Store the data messages and the commit or abort markers in their own logs.
Group coordinator
A different helper that stores consumer bookmarks. Bookmark updates can join a transaction.
Read-committed consumers
Readers that hide unfinished and cancelled transactions.
Two different coordinators exist in Kafka: the group coordinator for consumer groups and the transaction coordinator for transactions. They sound alike, but they do different jobs and keep different logs.
Idempotence Under the Hood: Sequence Numbers and Retries
The duplicate-catching machinery that transactions rely on every second.
Each partition leader remembers, for each producer ID, the last few sequence numbers it accepted. When a new batch of messages arrives, the leader compares its first sequence number with what it expects next. There are only three possible situations, and each has a clear response.
The next number in order
The batch is accepted and appended, and the expected number moves forward.
A number already seen
The batch is a retry of something already stored. The leader replies “success” without storing a second copy.
A number that skips ahead
Something was lost in between. The leader reports an out-of-order error so the producer can react.
Why several batches can be in flight
To keep the network busy, a producer may send several batches without waiting for each reply. Kafka allows a small number of such in-flight batches per connection while still preserving order, because the sequence numbers let the leader detect any reordering after a failed send. This is why the producer’s order-preserving retries are safe when idempotence is on.
Think of a factory conveyor belt where every box carries a running number. The packing table checks each number as boxes arrive. A repeat number is a duplicate to be discarded, and a missing number is a signal to stop and investigate.
How it connects to transactions
Transactions use the same producer ID, epoch, and sequence numbers that idempotence uses. What transactions add is the coordinator, which makes the producer ID survive restarts through the stable transactional ID, and which decides the final fate of all the numbered messages in a transaction. Idempotence is the foundation, and the coordinator is the roof.
Modern producers turn idempotence on by default. Even if you never use transactions, this quiet feature already protects your retries from creating duplicates within a partition.
Three Names: Transactional ID, Producer ID, and Epoch
These three labels are easy to mix up. Here is a simple way to keep them apart.
| Label | Who chooses it | How long it lasts | What it is for |
|---|---|---|---|
| Transactional ID | Your application | Stable across restarts, until it expires from idleness | Names the logical producer so the coordinator can recognize it after a crash |
| Producer ID | The coordinator | Tied to the transactional ID | Used inside messages so partitions can detect duplicates |
| Epoch | The coordinator | Goes up each time a new instance takes over | Lets the system lock out older copies of the same producer |
Think of a school badge system. The transactional ID is a person’s permanent name, such as “Asha Verma.” The producer ID is the badge number printed on the badge. The epoch is the badge’s version sticker. When a new badge is issued because the old one was lost, the version sticker goes up, and any security guard who sees the old version knows to refuse it.
Why a stable name is required
When a producer crashes, a new instance starts and uses the same transactional ID. The coordinator sees a familiar name, gives it the same producer ID, and raises the epoch. In doing so, it also cleans up: if the earlier instance left a transaction half-finished, the coordinator aborts it before letting the new instance continue. This is how a restart avoids leaving stale work behind.
Choose transactional IDs that are stable and meaningful, tied to the job the producer does, for example the name of the task plus the input partition it handles. Random IDs generated at every start defeat the whole purpose.
The Life Cycle of a Transaction: States
Every transaction moves through a small set of named states, and the coordinator records each move.
stateDiagram-v2
[*] --> Empty
Empty --> Ongoing : first partition added
Ongoing --> Ongoing : more partitions or offsets added
Ongoing --> PrepareCommit : producer asks to commit
Ongoing --> PrepareAbort : producer asks to abort or timeout expires
PrepareCommit --> CompleteCommit : markers written to all partitions
PrepareAbort --> CompleteAbort : markers written to all partitions
CompleteCommit --> Empty : ready for next transaction
CompleteAbort --> Empty : ready for next transaction
Empty --> Dead : transactional ID expires from idleness
Empty
No transaction is open. The producer is registered and waiting to begin.
Ongoing
A transaction is open and collecting writes and offset updates.
Prepare Commit / Prepare Abort
The decision has been written to the log. It can no longer be changed, only carried out.
Complete Commit / Complete Abort
Every involved partition has received its marker. The transaction is finished.
Dead
The transactional ID has been idle so long that the coordinator forgot it.
Prepare Epoch Fence
A temporary state used while the coordinator locks out an older copy of the producer.
Think of sending a parcel: “being packed” (Ongoing), “sealed and label printed” (Prepare), “delivered to every address” (Complete). Once the label is printed and the seal is on, the parcel cannot be re-packed; it can only be delivered.
Inside the Coordinator’s Notebook: What Is Stored for Each ID
A closer look at the small record that the coordinator keeps for every transactional ID.
For each transactional ID, the coordinator keeps one small record, both in memory and in the transaction log. Think of it as an index card. It is not big, but it holds everything needed to recover the transaction from any point.
| Field on the card | What it holds | Why it is needed |
|---|---|---|
| Transactional ID | The application-chosen name | The key that finds the card |
| Producer ID | The number stamped on the producer’s messages | So partitions can recognize the producer |
| Producer epoch | The current version of the producer | To lock out older copies |
| Transaction timeout | The limit the producer asked for | To know when to abort a stuck transaction |
| State | Empty, Ongoing, Prepare, Complete, or Dead | To know what to do next after a crash |
| Partition list | Every partition touched by the open transaction | To know where to send markers |
| Start and last update times | When the transaction began and last changed | To apply timeouts and expire idle IDs |
A hospital keeps one chart per patient at the foot of the bed. The chart tells any new nurse the patient’s name, the current treatment stage, and what to do next. The nurse on the night shift needs no extra briefing, because the chart is complete.
Every change is a new card
The coordinator never edits a card in place. Each change, such as adding a partition or moving to a Prepare state, is written as a new version of the card at the end of the transaction log. Because the log is compacted, older versions of the same card are cleaned up over time. When a new coordinator loads its partition, it reads the cards in order, and the last version of each card wins.
Why the state names matter for recovery
After a crash, the state on the card tells the new coordinator exactly what remains to be done. An Ongoing card means “wait for the producer, or abort on timeout.” A Prepare card means “the decision is made, finish delivering markers.” A Complete card means “nothing to do.” This simple rule turns recovery from a puzzle into a lookup.
When you see confusing behavior after a broker restart, think of the card: which state was it in, and what does that state say the coordinator should do next? Most puzzles become simple.
The Whole Flow, Step by Step
One complete transaction from the first request to the final marker.
sequenceDiagram
participant P as Producer
participant TC as Transaction Coordinator
participant TL as Transaction Log
participant PL as Partition Leaders
P->>TC: FindCoordinator for my transactional ID
TC-->>P: I am the coordinator
P->>TC: InitProducerId
TC->>TL: Save producer ID and new epoch
TC-->>P: Producer ID and epoch
Note over P: beginTransaction is a local step
P->>TC: AddPartitionsToTxn for orders-0 and stock-3
TC->>TL: Save state Ongoing with partitions
TC-->>P: OK
P->>PL: Produce messages marked as transactional
PL-->>P: Acknowledged
P->>TC: EndTxn commit
TC->>TL: Save PrepareCommit
TC-->>P: Commit accepted
TC->>PL: WriteTxnMarkers commit
PL-->>TC: Markers written
TC->>TL: Save CompleteCommit
Find the coordinator
The producer asks any broker “who is the coordinator for my transactional ID?” and gets an address.
Initialize
The producer asks the coordinator for its producer ID and epoch. Old unfinished work is cleaned up here.
Begin
The producer marks the start of a transaction. This step happens inside the client and needs no message to the broker.
Register partitions
Before writing to a new partition, the producer tells the coordinator, which records it in the log.
Write the data
Messages go straight to partition leaders, flagged as part of a transaction.
Ask to end
The producer asks the coordinator to commit or abort.
Decision then delivery
The coordinator writes the decision, replies, and then writes markers to every partition. Finally it records completion.
Booking a group holiday through a travel agent: you register with the agent, tell the agent which hotels and flights you will use, book each one, and finally say “confirm all.” The agent writes the confirmation in her book first, then telephones every hotel and airline to finalize.
Step 1 and 2: Finding the Coordinator and Getting an Identity
The first conversation sets up everything that follows.
When the application starts, it creates a producer with a transactional ID and calls the initialization command. Behind the scenes, the client asks any broker to find the coordinator for that ID. The broker computes the partition of the transaction log that owns the name and answers with the address of that partition’s leader.
The producer then asks that broker for an identity. The coordinator looks in its memory for this transactional ID. If it has never seen the name, it creates a fresh producer ID and starts the epoch at zero. If it has seen the name before, it keeps the producer ID and increases the epoch by one. It writes the result into the transaction log, waits until the copies are safe, and replies.
First time
A new producer ID and epoch zero. The state is Empty.
Restart or takeover
Same producer ID, higher epoch. Any earlier open transaction is finished off first.
Cleaning up the past
If the previous instance was in the middle of a transaction, the coordinator must finish it before answering. If the old transaction had already reached the “prepare commit” state, the coordinator completes the commit. If it was only ongoing, the coordinator aborts it. Only when the slate is clean does the new producer get its new epoch. This is how a restarted program never sees half-finished work from its earlier self.
A new night guard arriving at a warehouse first gets a walk-through: “Here are the doors the last guard left open. We closed them before you took over.” Only then does the guard start the shift with a fresh badge.
Call the initialization command once per producer, right after creating it. Doing it again later in the same run is wasteful and can raise the epoch unnecessarily.
Step 3: Registering Partitions and Writing Data
The coordinator must know every partition a transaction touches, or it cannot finish the job later.
Suppose the producer wants to write to the “orders” topic, partition 0, for the first time within the current transaction. Before sending a single message, the producer tells the coordinator “I will write to orders-0.” The coordinator changes the transaction’s state to Ongoing if needed, records the partition in its list, saves the update in the log, and replies. Only then does the producer send the messages.
Register once per partition
The producer asks only the first time it touches a partition within a transaction. Later writes to that partition need no new registration.
Messages carry the identity
Every message is stamped with the producer ID, the epoch, and a sequence number, and it is flagged as transactional.
Partition leaders store them normally
The messages go into the partition log in order, replicated like any other. The messages exist on disk but are not yet visible to strict readers.
The coordinator keeps the list
The list of registered partitions is what the coordinator will later use to send its markers.
Before you leave luggage at different hotels on a tour, you tell the tour manager the name of each hotel. When the tour ends, the manager knows exactly which hotels to inform that the trip is finished. A hotel that was never registered would never hear the news.
Newer Kafka releases add extra checks on the broker side, so a partition leader can confirm that a partition truly belongs to the transaction before accepting messages. This guards against rare delayed messages from a slow old request. Keep clients and brokers reasonably current to benefit.
Step 4: Committing, Kafka’s Two-Phase Approach
The coordinator decides first, and delivers second. That order is the secret of safety.
flowchart TD
A["Producer asks to commit"] --> B["Coordinator saves PrepareCommit in the transaction log"]
B --> C{"Saved on enough copies?"}
C -->|"No"| B
C -->|"Yes"| D["Point of no return - transaction WILL commit"]
D --> E["Reply to producer - commit accepted"]
D --> F["Send commit markers to every registered partition"]
F --> G{"All partitions confirmed?"}
G -->|"No - retry"| F
G -->|"Yes"| H["Save CompleteCommit in the transaction log"]
H --> I["Transaction finished - ready for next one"]
Phase one: decide and record
The coordinator writes “PrepareCommit” to the transaction log. When enough copies confirm, the decision is permanent. Even if the coordinator broker dies a moment later, the next coordinator will read this entry and finish the commit.
Tell the producer
Now the coordinator can safely tell the producer that the commit was accepted. From this point the producer does not need to wait for the markers.
Phase two: deliver the markers
The coordinator sends a small commit marker to the leader of every partition on the registered list. Each leader appends the marker to its log.
Record completion
After every partition confirms, the coordinator writes “CompleteCommit.” The bookkeeping for this transaction is done.
A judge writes the verdict in the court record first. Only then do clerks carry copies to every office that needs to act. If a clerk falls ill, another clerk picks up the record and delivers the copies. Nobody can argue about what the verdict was, because it is written down.
Why the order matters
If the coordinator delivered markers first and wrote the decision later, a crash in between could leave some partitions with “commit” and the coordinator with no memory of deciding. By writing first, the decision cannot get lost, and delivering the markers becomes a job that can always be repeated safely.
Aborting: The Same Path, the Opposite Result
When work must be cancelled, the coordinator follows the same careful steps.
A transaction can be aborted by the producer itself, for example when an error occurs in the business logic. It can also be aborted by the coordinator, for example when the transaction stays open longer than allowed, or when a new instance of the same producer takes over. The path mirrors the commit: the coordinator saves “PrepareAbort,” delivers abort markers to every registered partition, and finally saves “CompleteAbort.”
| Question | Commit | Abort |
|---|---|---|
| Who asks? | The producer | The producer, or the coordinator on timeout or takeover |
| Decision saved in the transaction log first? | Yes | Yes |
| Marker written to every registered partition? | Yes, a commit marker | Yes, an abort marker |
| What do strict readers see? | The transaction’s messages | None of the transaction’s messages |
| Are the messages deleted from disk? | Not applicable | No, they remain until normal cleanup, but they are skipped by strict readers |
Cancelling a group holiday works the same as confirming one, only backwards: the agent records the cancellation and telephones every hotel. The hotels do not throw away the paperwork; they mark it “cancelled” so nobody mistakes it for a live booking.
Aborted messages take up disk space until Kafka’s usual retention removes them. If your application aborts very often, this wasted space and the extra skipping work for readers become a real cost, so treat frequent aborts as a bug to fix.
Transaction Markers and Control Records
The tiny records that tell every reader whether a transaction won or lost.
A transaction marker is a special record placed in a partition’s log at the end of a transaction. It contains no business data. It only says “the transaction from producer ID so-and-so has been committed” or “…aborted.” Because it is a real record in the log, it occupies an offset number, which is why you may notice that offsets sometimes skip a number in transactional topics. Readers never see the markers as messages; they use them as signposts.
Transactional data records
Your real messages, stamped with the producer ID, epoch, and a transaction flag.
Commit marker
Says the earlier flagged messages from this producer are now official.
Abort marker
Says the earlier flagged messages from this producer must be ignored by strict readers.
In a book of receipts, a clerk stamps “VOID” or “PAID” on the last page of a batch. Readers do not read the stamp as a receipt, but they use it to know how to treat all the receipts before it.
Why markers live in each partition
A reader only talks to partition leaders, not to the coordinator. So the outcome must be written where the reader can see it: in the partition itself. The coordinator’s log holds the master truth, and the markers are the copies of the truth distributed to each place that needs it.
Programs that count messages by subtracting offsets will be off by a little in transactional topics because of these markers. Never assume that offsets are consecutive messages.
Reading Committed Data: Isolation Levels and the Stable Offset
The other half of the promise: readers must choose to respect transactions.
Consumers have a setting called the isolation level. In the default level, called read-uncommitted, a consumer sees every message as soon as it is written, including messages of open transactions and even messages of transactions that will be aborted later. In the strict level, called read-committed, the consumer sees only messages of committed transactions, plus ordinary messages that were not part of any transaction.
The last stable offset
Each partition leader tracks a position called the last stable offset. It sits just before the earliest transaction that is still open. Strict readers are never given anything beyond it.
An index of aborted transactions
When the broker sends a batch of messages, it also lists which transactions in that batch were aborted.
The client filters
The consumer library uses that list to drop the aborted messages before your code sees them.
Order is preserved
Messages appear in offset order, and a committed transaction becomes visible at the position of its data, not its marker.
Imagine a printing press that lays out pages in order but keeps one page “under review.” Strict readers cannot go past the page under review, even if later pages are already finished. They wait until the review is done, then move forward together.
The cost of being strict
Because strict readers cannot go past the last stable offset, a single long-running open transaction can hold back everything after it in that partition, even messages from other, already committed transactions. This is why transactions should be short. A transaction that stays open for minutes creates minutes of delay for every strict reader of the partitions it touched.
Any consumer that is meant to see only reliable results, such as a downstream payment service, must use the strict setting. Otherwise it may act on results that are later cancelled.
A Worked Example: Offsets, Markers, and the Stable Offset
Let us watch one partition through a short story with real numbers.
Imagine one partition that is empty. Two transactional producers, Asha and Ravi, write to it at about the same time. We will follow the offsets one by one and see what a strict reader and a normal reader each see at every moment.
| Moment | What happens in the partition | Last stable offset | Strict reader sees | Normal reader sees |
|---|---|---|---|---|
| 1 | Asha writes three messages at offsets 0, 1, and 2 inside her transaction | 0 | Nothing yet | Offsets 0 to 2 |
| 2 | Ravi writes two messages at offsets 3 and 4 inside his transaction | 0 | Nothing yet | Offsets 0 to 4 |
| 3 | Asha commits. A commit marker is written at offset 5 | 3 (Ravi is still open) | Offsets 0 to 2 | Offsets 0 to 4 |
| 4 | Ravi’s transaction is still open and time passes | 3 | Nothing new, even if more messages arrive after | Everything, including later messages |
| 5 | Ravi aborts. An abort marker is written at offset 6 | 7 (nothing open) | Offsets 3 and 4 are skipped as aborted; markers hidden | Offsets 3 and 4 still appear as ordinary messages |
Ravi’s open transaction is the wall
Even though Asha’s work was committed, the strict reader cannot pass offset 3 while Ravi’s earliest message sits there without a marker. Asha’s messages before that wall are fine, but anything after it waits.
Markers use up offsets
Offsets 5 and 6 belong to markers. No business message lives there, which is why offsets in transactional partitions have small gaps.
The filter happens in the client
The broker hands the strict reader the messages up to the stable offset together with a list of aborted transactions. The library then drops the aborted ones.
Two students are handing in homework to the same tray. The teacher only marks pages once each student’s stack is sealed. One student’s stack is sealed early, but the teacher cannot rush past the second student’s half-finished stack that sits in the middle of the tray, so marking pauses at that point.
This example shows why one slow producer can slow strict readers of a shared partition. If several transactional producers write to the same partition, the slowest open transaction sets the pace for everyone reading strictly.
Consume, Transform, Produce: Saving the Bookmark Inside the Transaction
This is where transactions deliver true exactly-once processing inside Kafka.
In an ordinary program, the consumer saves its bookmark in one step and the producer writes outputs in another. A crash between the steps causes either lost work or duplicated work. In a transactional program, the producer tells the transaction “please include these bookmark updates,” and the bookmark and outputs commit together or not at all.
Read a batch
The consumer reads messages, with automatic bookmark saving turned off and with the strict isolation level.
Begin and write
The producer begins a transaction and writes the results to the output topics.
Add the bookmark to the transaction
The producer tells the coordinator that the transaction includes bookmark updates for a certain consumer group. The coordinator registers the internal bookmark partition as part of the transaction.
Send the bookmark values
The producer sends the new offsets to the group’s coordinator, marked as part of the transaction, so they stay pending.
Commit
The transaction coordinator commits, and markers are written to the output partitions and the bookmark partition alike. The new bookmark and the outputs become real together.
A warehouse worker takes items from an inbox, packs them into outgoing boxes, and moves the “done up to here” flag on the inbox. If all three actions are in one sealed tray, then a crash means the whole tray is restarted, never a half-tray.
The producer also passes the consumer group’s current membership details when saving the bookmark. This lets Kafka reject a bookmark update from a consumer that has already been removed from the group, which closes another door to duplicates.
Zombies and Fencing: Locking Out Old Copies
What if an old instance of your program wakes up after a new one has already taken over?
Programs sometimes freeze for a long time because of a network pause, a heavy garbage collection, or a stuck machine. Meanwhile, the orchestration system may decide the program is dead and start a replacement. Later the frozen program wakes up and continues as if nothing happened. Such a revived old instance is called a zombie. If two copies write the same outputs, duplicates and confusion follow.
The replacement starts
It uses the same transactional ID and asks for an identity. The coordinator raises the epoch, say from 4 to 5, and aborts any leftover transaction of the old one.
The zombie wakes up
It still believes it has epoch 4 and tries to add partitions or send messages.
Everyone compares epochs
The coordinator and partition leaders know the newest epoch is 5. A request stamped with 4 is refused with a “producer fenced” error.
The zombie must stop
The client treats this error as fatal and shuts down that producer, leaving the replacement in charge.
A security guard finds that the old key card no longer opens any door because the building issued a new card version. The former employee can walk up to every door and press the card, but none opens. Only the holder of the newest version passes.
Why this is such a strong protection
Fencing does not depend on clocks or on guessing who is alive. It depends on a number that only goes up and that every part of the system checks. This idea, used across many distributed systems, is one of the most reliable ways to prevent two active copies from causing damage.
Never run two live instances with the same transactional ID on purpose. If you do, they will fence each other again and again, and neither will make progress. Each running producer needs its own ID, or a clear takeover relationship.
Timeouts and Expiration: Stopping Stuck Transactions
Every open transaction is a small promise to finish soon. The coordinator enforces that promise.
| Setting idea | Where it lives | Meaning | Typical default |
|---|---|---|---|
| Transaction timeout | Producer | How long the producer allows a transaction to stay open before the coordinator may abort it | About one minute |
| Maximum transaction timeout | Broker | The largest timeout any producer is allowed to request | About fifteen minutes |
| Abort cleanup interval | Broker | How often the coordinator looks for timed-out transactions | Every few seconds |
| Transactional ID expiration | Broker | How long an idle transactional ID is remembered before it is forgotten | About seven days |
These are typical defaults and may vary by version, so check the documentation for yours. The important idea is that the coordinator does not trust producers to finish. If the producer disappears with a transaction open, the coordinator aborts it after the timeout, which releases the readers who were waiting behind it.
A restaurant holds a table for a party for fifteen minutes. If nobody arrives, the host releases the table so the queue can move. Without this rule, one no-show would block all the customers behind them.
Choosing the timeout
Set the transaction timeout comfortably longer than your longest normal transaction, but not much longer. Too short, and healthy but slow transactions get aborted. Too long, and a crashed producer can hold back strict readers for a long time. If your transactions normally take a second, a timeout of several seconds to a minute is usually plenty.
Forgetting idle IDs
If a transactional ID is unused for the full expiration time, the coordinator forgets it to save space. When the ID returns later, it is treated as new. This matters for rare batch jobs that run less often than the expiration time. They should not depend on remembered state between runs.
When the Coordinator Broker Fails: Failover and Recovery
A referee who can collapse must have a replacement who knows exactly where the game stood.
Because the coordinator’s memory lives in a replicated log, losing a coordinator broker never loses the transactions. When the broker that leads a partition of the transaction log fails, the cluster’s own leader election picks a new leader for that partition from the copies that are fully caught up. In modern Kafka, the active controller of the KRaft controller quorum performs this choice and records it in the metadata log, exactly like any other partition.
The coordinator broker goes silent
Its heartbeats to the controller stop. After the session timeout, the controller declares the broker unavailable.
A new leader is chosen for the log partition
The controller picks a fully caught-up copy and records the change. The chosen broker becomes the new coordinator for all transactional IDs stored in that partition.
The new coordinator loads its state
It reads the transaction log partition from the start of the compacted data and rebuilds an in-memory table: every ID, its producer ID, epoch, state, and registered partitions.
Unfinished decisions are completed
Any transaction found in a Prepare state is finished: the markers are sent again to every registered partition. Repeating a marker is harmless.
Producers reconnect
Producers that receive a “not the coordinator” error ask again, find the new coordinator, and continue their work.
If a chess referee has to leave, a new referee takes the official scoresheet, reads what has happened, and continues. If the old referee had just announced “checkmate” and written it down, the new referee simply completes the ceremony.
Why loading takes a moment
Rebuilding memory from the log is not instant. For a coordinator responsible for a large number of transactional IDs, loading can take noticeable time, during which requests for those IDs receive a “coordinator loading” answer and the producer retries. Good clients handle this automatically. Keeping the number of transactional IDs reasonable and the log compacted keeps loading fast.
Test broker failure during a busy period in a staging environment. Watch how long producers stall and confirm that no committed data is lost and no transaction is left half-delivered.
Failure Scenarios, One by One
A design is only as good as the way it behaves when things go wrong.
Scenario 1: The producer crashes before committing
The transaction stays open until its timeout passes, or until a new instance with the same transactional ID starts. Either way, the coordinator aborts it. Abort markers are written, strict readers skip the messages, and the new instance redoes the work from the saved bookmark.
Scenario 2: The producer crashes right after asking to commit
If the coordinator had already saved the decision, the commit will be completed even though the producer is gone. If the request never arrived, the transaction is aborted later. Both outcomes are clean and consistent.
Scenario 3: The coordinator crashes after saving PrepareCommit
The new coordinator reads the log, sees the decision, and sends the markers again. The transaction commits exactly as promised.
Scenario 4: The coordinator crashes before saving the decision
There is no decision to complete. The producer’s commit request fails with a retriable error, the producer finds the new coordinator, and it asks again. If it can no longer be sure, it aborts and redoes the batch.
Scenario 5: A partition leader is unavailable when markers are sent
The coordinator keeps retrying until a new leader for that partition is elected. The transaction stays in the Prepare state during that time. Strict readers of that partition wait, but no data is lost.
Scenario 6: A network split isolates a zombie producer
When it returns, its old epoch is rejected. It is fenced and must stop. The replacement continues cleanly.
Scenario 7: Too few in-sync copies of the transaction log
If fewer than the required number of copies are in sync, the coordinator cannot safely save its state, and transaction operations for those IDs fail until the copies recover. This is a deliberate choice: safety first.
Setting the transaction log’s minimum in-sync copies to one to “improve availability” quietly removes the safety of the whole feature. Keep the defaults of three copies with two required in sync on production clusters.
Hanging Transactions: The Quiet Troublemaker
A transaction that neither finishes nor gets cleaned up can freeze readers for a long time.
A hanging transaction is a transaction whose data messages are in a partition, but whose marker never arrived. Strict readers cannot move past it, because the last stable offset sits in front of it. Normally the coordinator’s timeout prevents this. In rare cases, such as certain software bugs, unusual failure sequences, or very old client versions, a transaction can be left hanging even though the coordinator no longer remembers it.
Notice the symptom
Strict consumers on a partition stop making progress, and their lag grows, while ordinary readers of the same partition keep going.
Look for open transactions
Kafka ships an administration tool that can list transactions, describe a partition’s active producers, and search for hanging ones.
Confirm it is really stuck
Check the transaction’s age against the timeout. A young transaction is probably just working.
Resolve it carefully
The same tool can ask for a stuck transaction to be aborted. Do it only after you are sure that the owning producer is gone.
A “reserved” sign is left on a table long after the customers left. The queue builds up outside because the host does not know the sign is stale. Someone has to walk over, check that nobody is sitting there, and remove the sign.
Newer Kafka releases add extra protection to shrink the chance of hanging transactions. Keeping clients and brokers up to date is the best prevention, and a short runbook for finding and aborting hanging transactions is the best cure.
Exactly-Once in Kafka Streams, Flink, and Kafka Connect
Most teams never call the transaction commands directly. They use them through bigger tools.
Kafka Streams
One configuration switch turns on exactly-once processing. The library handles transactional IDs, reading strictly, saving bookmarks in the transaction, and committing on a short timer. Its newer mode uses one producer per processing thread instead of one per task, which greatly reduces the number of transactional IDs.
Apache Flink
Flink’s Kafka sink can write with transactions that commit when a checkpoint completes, so output becomes visible only when the state of the whole job is safely saved.
Kafka Connect
Newer versions can run source connectors with exactly-once delivery by writing the source data and the connector’s progress notes in one transaction.
Your own services
Any program that consumes, transforms, and produces can use the transactional client directly for the same effect.
You do not need to know how a car engine’s fuel injection works to drive to work. Streams and Flink are the car. The coordinator is the injection system beneath the hood, working quietly every time you press the accelerator.
How the commit timer affects everything
With exactly-once processing turned on, Streams commits often, by default about every hundred milliseconds instead of every thirty seconds. Each commit is a full transaction. A shorter interval reduces delay for strict readers but increases the number of transactions per second, so it costs more coordinator work. A longer interval does the opposite. This one setting is the main dial for trading latency against overhead.
Because these tools hide the details, problems often appear as “consumers are lagging” or “processing is slow” rather than as an obvious transaction error. Knowing about the coordinator helps you connect the symptom to the cause.
The Limits of Exactly-Once: Outside Systems
Kafka transactions protect what happens inside Kafka. Everything outside needs extra care.
Suppose a program reads an order from Kafka and also sends a confirmation email. The email service is not part of the Kafka transaction. If the program sends the email and then crashes before committing, the restart repeats the work and sends the email again. Kafka cannot take back an email.
| Technique | How it helps | Analogy |
|---|---|---|
| Make the outside action idempotent | Give each action a unique key so a repeat is ignored | A ticket number that a cinema accepts only once |
| Outbox pattern | Write the intended action into Kafka inside the transaction, and let a separate reliable worker perform it | Leaving a note in an out-tray for the postman |
| Transactional sink connectors | Store the result and the position together in the target database | Stamping the receipt and the register in one motion |
| Reconciliation | Compare both sides regularly and fix differences | An end-of-day cash count |
Do not tell business colleagues that “Kafka guarantees exactly once” without explaining the boundary. Money and messages leaving Kafka need their own protection.
Performance and Scalability
Transactions add safety, and safety costs a little work. Here is where the cost goes.
Coordinator round trips
Each transaction needs a few extra messages: registering partitions and ending the transaction. These are small but add up when transactions are tiny and frequent.
Log and marker writes
The coordinator writes several entries to its log per transaction, and every involved partition receives a marker.
Visibility waits for commit
Strict readers see data only after the commit. Shorter transactions mean lower delay.
Bigger batches, fewer transactions
Doing more work per transaction spreads the fixed cost across more messages, which raises throughput.
Finding a balance
Very small transactions, such as one message each, waste effort: the coordinator work dwarfs the data. Very large transactions hold back strict readers and risk timeouts. A good rule is to group as many messages as fit within your acceptable delay, often somewhere between tens of milliseconds and a few seconds.
How the design scales
The coordinator’s work is spread across the fifty partitions of the transaction log, and therefore across many brokers. The data itself never passes through the coordinator; it goes straight to partition leaders. So adding brokers and partitions increases capacity without making one central machine a bottleneck. The main things that grow with scale are the number of transactional IDs and the number of partitions touched per transaction.
A courier who takes one letter per trip spends the whole day on the road. A courier who collects a bag of letters makes the same trip but delivers hundreds. Batching is the same idea for transactions.
Measure before you tune. Record the transaction rate, the commit time seen by producers, and the lag of strict consumers. Then change one setting, such as the commit interval, and measure again.
Sizing the Transaction Log and the Coordinator Load
How much memory, disk, and attention does the coordinator need?
The coordinator keeps a small entry in memory for every transactional ID it manages, containing the producer ID, the epoch, the state, and the current list of partitions. Even a large number of IDs uses modest memory. The transaction log on disk is compacted, so it holds roughly one latest entry per ID, plus recent history not yet cleaned.
| Factor | What grows | What to do |
|---|---|---|
| Number of transactional IDs | Memory and loading time after failover | Reuse stable IDs, avoid random new IDs per run, and let idle ones expire |
| Transactions per second | Coordinator requests and log writes | Batch more work per transaction |
| Partitions per transaction | Markers to send and state to store | Avoid touching hundreds of partitions in one transaction |
| Transaction log partition count | Spread of coordinator work | Choose it once at the start, and never change it |
| Replication settings of the log | Durability and availability | Keep three copies with two required in sync |
A hotel front desk keeps a small card for each guest. A hotel with more guests needs more cards and more desk clerks, but it does not need a bigger building for the cards. What matters is how fast guests check in and out.
A special case: many short-lived IDs
An anti-pattern is to create a new transactional ID for every request or every job run. The coordinator then accumulates a huge number of entries that each live for a week before expiring. This wastes memory, slows failover, and adds no benefit. Design a small, stable set of IDs instead.
Advantages, Disadvantages, and Trade-Offs
An honest look at what transactions give you and what they cost.
Advantages
- All-or-nothing writes across topics and partitions
- No duplicates from retries or restarts inside Kafka
- Bookmarks and outputs saved together
- Old copies of a producer are locked out automatically
- No separate service to run; the coordinator lives in the brokers
- State survives failures because it is stored in a replicated log
Disadvantages
- Extra latency, because readers wait for the commit
- Extra coordinator work per transaction
- Long or stuck transactions can hold back strict readers
- Guarantee stops at the edge of Kafka
- More settings to understand and monitor
- Aborted data still uses disk until retention removes it
Strict readers wait
You get clean results, but you accept a delay of about one transaction length.
Log needs enough copies
If the transaction log cannot be safely written, transactions pause instead of guessing.
Use a framework or call directly
Frameworks hide details and reduce mistakes. Direct use gives control but needs more care.
Batch size
Bigger transactions raise throughput but delay visibility. Smaller ones do the opposite.
Versions and Improvements Over Time
Kafka’s transaction feature has been refined many times since it first appeared.
The first release (around version 0.11)
Idempotent producers, transactions, and the coordinator arrived together, based on the original design proposal known as KIP-98.
Simpler exactly-once for Streams (around 2.5)
A follow-up change allowed one producer per thread instead of one per task, using consumer group information to keep fencing safe.
Better tooling (around 3.0)
An administration tool arrived to list, describe, and clean up transactions, including finding hanging ones.
Stronger server-side defenses (3.x to 4.0)
Later proposals, known as KIP-890, add checks on the broker so that delayed or stray requests cannot corrupt a transaction, and reduce the chance of hanging ones.
Think of a bank’s payment system. The first version worked, but over the years the bank added better receipts, faster checks, and extra guards at the doors. The idea stayed the same, but the walls got thicker.
Exact version numbers and defaults change over time. Always confirm behavior in the release notes and documentation for the versions you run, and keep clients and brokers close to each other in version.
Configuration Checklist for a Production Cluster
The settings that matter, grouped by who owns them and described by their meaning.
| Owner | Setting idea | Recommended approach |
|---|---|---|
| Broker | Number of partitions in the transaction log | Keep the default unless you have a very large cluster, and never change it later |
| Broker | Replication factor of the transaction log | Three copies for production |
| Broker | Minimum in-sync copies of the transaction log | Two, so a write is safe on at least two brokers |
| Broker | Maximum transaction timeout | Set an upper limit that matches your longest legitimate transaction |
| Broker | Transactional ID expiration | Longer than the gap between runs of your slowest job |
| Producer | Transactional ID | Stable, unique per logical producer |
| Producer | Transaction timeout | A little longer than your longest normal transaction |
| Producer | Acknowledgments | Wait for all in-sync copies |
| Producer | Idempotence | On, which is required for transactions |
| Consumer | Isolation level | Strict, for anything that must see only committed data |
| Consumer | Automatic bookmark saving | Off in consume-transform-produce programs, since the transaction saves bookmarks |
| Streams | Processing guarantee | Use the newest exactly-once mode available in your version |
Before opening a new restaurant you check the fire exits, the fridge temperature, the staff schedule, and the cash register. Each item is simple, but forgetting one causes trouble on opening night.
Small cluster warning
Small development clusters with fewer than three brokers cannot honor the default replication of the transaction log. Some people lower the settings in development to make things start. That is fine for learning, but never carry those reduced settings to production, because the safety of the whole feature depends on them.
The setting names and defaults vary between versions. Verify each one in the documentation for your release, and store your final settings in version control so changes are visible and reviewable.
Security of Transactions
A producer that can run transactions can influence what many readers see, so its rights deserve care.
Kafka’s access rules treat the transactional ID as a resource, just like a topic. A producer needs permission to use its own transactional ID, permission to write to the topics it touches, and, when it saves consumer bookmarks inside the transaction, permission to read the consumer group. Idempotent writes also require a cluster-level right on older versions.
Grant the transactional ID right narrowly
Give each application permission only to its own transactional IDs, using a prefix such as the application name. One application should never be able to use another’s ID.
Grant write access only to needed topics
Limit the topics a transactional producer may write to. A compromised producer could otherwise commit unwanted data anywhere.
Encrypt and authenticate connections
Use encrypted connections and strong identities between clients and brokers, and among brokers, since the coordinator sends markers between brokers.
Protect the internal log
Nobody except the brokers themselves should be able to read or write the transaction log topic directly.
Restrict administrative tools
The tool that can abort transactions is powerful. Limit who may run it and record every use.
In a company, only a few people are allowed to sign cheques, and each person can sign only for their own department. A transactional ID is like a signing authority: it should be personal to the job and never shared casually.
Giving every application permission for all transactional IDs “to make it work” lets one faulty program fence, abort, or disturb the transactions of all the others.
Monitoring, Logging, and Metrics
What to watch so you notice trouble before your users do.
Monitoring transactions comes down to four questions: Are transactions completing? Are they fast? Are any stuck? Are the coordinators healthy? Kafka exposes measurements for each, though exact names differ between versions, so treat this as a guide to the ideas.
| What to watch | Where it appears | Warning sign |
|---|---|---|
| Time to begin, commit, and abort | Producer metrics | Commit time creeping up |
| Rate of aborted transactions | Application metrics and logs | A rising share of aborts |
| Producer fenced errors | Application logs | Repeated fencing, which suggests two instances share an ID |
| Lag of strict consumers versus normal consumers | Consumer metrics | Strict readers far behind on a partition |
| Age of the oldest open transaction | Administration tool or custom check | Older than your transaction timeout |
| Transaction log partition health | Broker metrics: under-replicated or offline partitions | Any non-zero value |
| Marker delivery queue sizes | Broker metrics for the transaction marker channel | Growing queues, meaning markers are not being delivered |
| Coordinator loading and not-coordinator errors | Client and broker logs | Long or frequent bursts |
Logs that tell the story
Coordinator logs mention state changes, timeouts, and epoch bumps for transactional IDs. Producer logs mention fencing, coordinator changes, and retries. Read them together in time order. A pattern such as “coordinator moved, loading, producer retried, committed” is normal. A pattern such as “fenced, restarted, fenced, restarted” points to two copies of the same program.
Add an alert on the age of the oldest open transaction. It is the earliest warning for hanging transactions, and it usually fires long before anyone complains about lag.
Tools and Day-to-Day Operations
A short map of what you will use, described by what each thing does.
Transactions administration tool
Lists transactional IDs, describes their state and open partitions, shows active producers on a partition, and searches for hanging transactions.
Abort option
The same tool can abort a specific hanging transaction. Use it only after confirming the owner is gone.
Log dump tool
Shows the raw contents of a log, including markers, useful for deep investigation.
Topic tools
Confirm the transaction log topic’s partition count, copies, and health.
Consumer group tools
Show bookmarks and lag, which help compare strict readers with normal ones.
Client metrics
Show the time spent in each transaction step from the application’s point of view.
A daily and weekly routine
Daily glance
Check the abort rate, the oldest open transaction age, and strict consumer lag on the main pipelines.
After every broker restart
Confirm there are no under-replicated partitions in the transaction log and that producers reconnected.
Weekly review
Look for growth in the number of transactional IDs, and for applications with unusual fencing or abort patterns.
Quarterly rehearsal
Kill a coordinator broker in a test environment and measure recovery. Practice the hanging-transaction procedure.
Keeping a car healthy means glancing at the dashboard every day, checking the oil weekly, and going for a service now and then. Transactions need the same gentle, regular attention.
Adopting Transactions in an Existing Pipeline
A gentle path from a simple at-least-once program to a transactional one.
Decide if you need it
List what would go wrong with duplicates or half-finished outputs. If the answer is “little,” simpler idempotent handling may be enough.
Upgrade the platform first
Bring clients and brokers to recent versions with the newer safety checks, and confirm the transaction log has three copies.
Turn off automatic bookmarks
Move progress-saving into the transaction, so the program controls when it happens.
Design the transactional IDs
Create a naming rule that stays stable across restarts and gives each running copy its own name.
Wrap the work loop
Read a batch, begin, write, add bookmarks, commit. On an ordinary error, abort and retry. On a fencing error, stop the producer.
Switch downstream readers to strict
Change consumers that depend on these results to see only committed data.
Test the failures
Kill the program mid-transaction, freeze it until it is replaced, and restart a broker. Check for no duplicates and no losses.
Roll out and watch
Start with one low-risk pipeline. Watch commit time, abort rate, and strict lag before moving to the next.
Switching a shop from handwritten receipts to a till system is not done in one afternoon. You test it with a few customers, watch for errors, and only then use it for the whole shop.
Write a test that deliberately crashes the program at each step of the loop, before the write, after the write, and just before the commit. If your outputs are correct after every crash, your design is sound.
Best Practices and Design Patterns
Habits that keep transactions calm and predictable.
Stable, unique transactional IDs
One per logical producer, the same across restarts, never shared by two live instances.
Keep transactions short
Commit within seconds. Short transactions mean small delays for strict readers and small losses if aborted.
Strict readers where it matters
Consumers that act on results should read only committed data.
Strong producer settings
Wait for all in-sync copies, allow retries, and keep idempotence on.
Default log replication
Three copies with two in sync for the transaction log.
Sensible transaction timeout
A little more than your longest normal transaction, and never more than needed.
Handle errors properly
Abort and retry on ordinary errors, and shut down cleanly on fencing errors.
Plan for the outside world
Design idempotent or outbox-style steps for anything that leaves Kafka.
Useful design patterns
Decide first, deliver second
The coordinator writes the decision durably before telling anyone. This pattern, found in many careful systems, makes recovery a simple matter of reading the record and finishing the job.
Fencing token
A number that only goes up and is checked everywhere. It removes the need to guess whether an old process is really dead.
Log as memory
Store the coordinator’s state in a replicated log. Recovery is replay, and there is no separate database to manage.
Timeout as a safety valve
Never let one silent participant block everyone else. A time limit turns an endless wait into a clean abort.
Anti-Patterns to Avoid
Three popular mistakes and the better ways to work.
What people do
They create a new random transactional ID every time the program starts, or for every request.
Why it hurts
The coordinator cannot recognize the returning producer, so it cannot clean up leftovers or lock out zombies. Memory fills with useless entries, and failover slows down.
Better approach
Use a small set of stable IDs that describe the job, such as application name plus task or input partition.
What people do
They hold a transaction open while waiting for slow outside calls, like a web service or a human approval.
Why it hurts
The last stable offset is stuck, strict readers stall, and the timeout may abort the work anyway.
Better approach
Finish the outside call first, then open a short transaction just to write the results. Or use the outbox pattern.
What people do
They read with the default isolation level downstream of a transactional pipeline and assume the data is always final.
Why it hurts
Those readers can see messages of transactions that later abort, and can act on data that never becomes official.
Better approach
Configure downstream consumers as strict readers wherever correctness matters.
Common mistakes in short
- Sharing one transactional ID between two running programs
- Setting the transaction log’s in-sync requirement to one
- Changing the transaction log’s partition count after use
- Committing single-message transactions at very high rates
- Ignoring aborted-transaction rates until disk use grows
- Assuming exactly-once extends to email, payments, or databases
Troubleshooting Playbook
From symptom to likely cause to first check.
| Symptom | Likely cause | First thing to check |
|---|---|---|
| Producer keeps getting “fenced” | Two live instances share one transactional ID, or a restart loop | How the ID is generated and how many copies are running |
| Strict consumers stall on one partition | A long-running or hanging transaction | Age of the oldest open transaction on that partition |
| Commit calls are slow | Slow coordinator log writes, busy brokers, or too many partitions per transaction | Broker disk and network health, and partitions touched per transaction |
| Errors saying “not coordinator” or “coordinator loading” | The coordinator recently moved | Recent broker restarts; these usually clear by themselves |
| Transactions time out | Timeout too short, or the producer is stuck | Real transaction duration compared with the timeout |
| Transaction operations fail cluster-wide | Transaction log lacks enough in-sync copies | Under-replicated partitions of the transaction log |
| Disk use grows on output topics | Many aborted transactions | Abort rate and the cause of aborts |
| Duplicates seen downstream | Readers are not strict, or the duplicate arises outside Kafka | Consumer isolation setting, and idempotency of outside steps |
Start with the simplest question: is one specific transaction stuck, or is everything slow? A single stuck transaction points to an application or a hanging case. Everything being slow points to brokers or the transaction log.
Real-World and Industry Examples
The same ideas appear in many well-known systems.
Stream processing at scale with Kafka Streams
Companies that build payment, fraud, and inventory pipelines on Kafka Streams turn on exactly-once processing so that each event affects the results once. Behind the scenes, this is the transaction coordinator committing outputs and bookmarks together many times per second.
Apache Flink jobs writing to Kafka
Flink’s Kafka sink can tie a Kafka transaction to each checkpoint. Output becomes visible only when the whole job’s state is safe, which is a classic two-phase commit design that maps naturally onto the Kafka coordinator.
Kafka Connect exactly-once sources
Source connectors that pull rows from a database or read files can, in newer versions, write records and their own progress notes in one transaction, so a crash never causes a gap or a repeat.
Financial and order-management systems
Banks, online retailers, and ride-hailing platforms are typical Kafka users where duplicates or half-finished updates cost real money. They usually combine Kafka transactions inside the event backbone with idempotent writes and reconciliation at the edges.
The wider pattern: two-phase commit
Relational databases, distributed databases, and messaging brokers all use variations of “record the decision, then deliver it.” Kafka’s approach is a streamlined version built on its own log.
Details of how specific companies configure their systems change over time and are often not public. Treat these examples as patterns rather than statements about particular deployments, and check current documentation.
A Day in the Life: QuickBite Payments
Everything you learned, told as one small story.
QuickBite is a food-delivery company. When a customer pays, a service called payment-processor reads a “payment-requested” event, checks the customer’s balance, and writes three things: a “payment-confirmed” event, a “ledger-entry” event, and a “notification-request” event. It also saves its bookmark on the input topic. All four must succeed together.
Morning: startup
The service starts with the transactional ID “payment-processor-3”. The coordinator recognizes the name, keeps its producer ID, and raises the epoch from 6 to 7. Nothing was left open from yesterday, so the state is clean.
Lunch rush: steady transactions
Every few hundred milliseconds the service batches a group of payments and commits them in one transaction. Strict downstream readers see confirmed payments only, a little after each commit.
Afternoon: a machine freezes
The service’s machine is overloaded and stops responding for a minute. The orchestration system starts a replacement with the same transactional ID. The coordinator raises the epoch to 8 and aborts the frozen instance’s open transaction.
The zombie wakes up
The old instance tries to continue with epoch 7. Every request is refused as fenced, and it shuts itself down. The replacement continues from the saved bookmark, so no payment is lost and none is confirmed twice.
Evening: a broker fails
The broker acting as the coordinator crashes. The controller elects a new leader for that partition of the transaction log. The new coordinator loads the state, finishes one commit that was already decided, and answers producers again within a few seconds.
Night: the email step
The notification service reads only committed events and sends emails using a unique request key, so a repeated event would not send a second email. The boundary of exactly-once is respected.
Notice how many failures the story contains, and how little the customers noticed. That is the point of the coordinator: to turn crashes into small delays instead of wrong money.
Glossary: Every Term in One Place
A quick reference for the words used in this guide.
| Term | Simple meaning |
|---|---|
| Transaction | A group of writes that all become visible together or not at all |
| Atomicity | The all-or-nothing property |
| Transaction coordinator | The broker-side referee that tracks and finishes transactions |
| Transactional ID | A stable name chosen by the application for a logical producer |
| Producer ID | A number the coordinator assigns to identify a producer in messages |
| Epoch | A counter that goes up when a new instance takes over, used to lock out old ones |
| Sequence number | A per-partition counter used to detect duplicate or missing messages |
| Idempotent producer | A producer whose retries never create duplicates in a partition |
| Transaction log | The internal compacted topic that stores the coordinator’s state |
| Compaction | Keeping only the newest record for each key |
| Commit | Making a transaction’s messages visible |
| Abort | Cancelling a transaction so its messages are ignored by strict readers |
| Transaction marker | A special record that marks a transaction as committed or aborted in a partition |
| Control record | A record that carries a system signal, not business data |
| Isolation level | A consumer setting that decides whether uncommitted data is visible |
| Last stable offset | The position before the earliest open transaction, beyond which strict readers cannot read |
| Fencing | Refusing requests from an outdated producer instance |
| Zombie | An old instance that wakes up after being replaced |
| Hanging transaction | An open transaction whose outcome was never delivered to a partition |
| Group coordinator | The broker role that manages consumer groups and their bookmarks |
| Consume-transform-produce | The pattern of reading, processing, and writing results with a saved bookmark |
| Outbox pattern | Recording an outside action in Kafka first and performing it later by a reliable worker |
| Exactly once | Each input has its effect one time, inside a defined boundary |
Frequently Asked Questions
Quick answers to the questions beginners ask most often.
Summary and Key Takeaways
The whole story in a few short lines.
Key Takeaways
- A role, not a machine: the transaction coordinator is a component inside brokers, chosen per transactional ID by the leader of one partition of the internal transaction log.
- Decide first, deliver second: the coordinator saves the commit or abort decision in its replicated log before sending markers to any partition, so no crash can lose or split the outcome.
- Identity plus epoch: a stable transactional ID, a producer ID, and a rising epoch let Kafka clean up after crashes and lock out zombie producers.
- Readers must opt in: only consumers with the strict isolation level hide open and aborted transactions, and they are held back by the last stable offset.
- Bookmarks join the transaction: saving consumer offsets inside the transaction gives exactly-once processing from Kafka to Kafka.
- Keep transactions short and stable: small, quick transactions, stable IDs, default log replication, and sensible timeouts prevent most problems, including hanging transactions.
- Know the boundary: exactly-once holds inside Kafka only. Outside systems need idempotent actions, outbox patterns, or reconciliation.