What Are Transactions in Kafka?
How Kafka makes a group of writes succeed or fail together: idempotent producers, transactional IDs, commit markers, read committed consumers, exactly-once processing and the limits you must respect in production.
Imagine you are moving money from your savings account to your checking account. Two things must happen: the savings balance goes down, and the checking balance goes up. If the bank’s computer crashes after the first step but before the second, your money has vanished. If it crashes and then retries, you might be charged twice. Nobody accepts “sometimes the money disappears” as an answer. The solution is a transaction: a bundle of steps that either all happen or none of them happen.
Apache Kafka faces the same problem. A program often reads a message, does some work, and writes results to several places. If it fails halfway, some results exist and others do not, and a retry may create duplicates. Kafka transactions solve this by letting a program write to many topics and partitions as one atomic unit. In this guide you will learn what transactions are, why they exist, how idempotent producers and transaction coordinators work, how consumers avoid seeing half-finished work, what “exactly-once” truly promises, and where the guarantees stop.
1What Transactions Mean in Kafka
A Kafka transaction is a group of writes, and optionally offset commits, that become visible together or not at all.
Start with the building blocks. A message is a small record of something that happened, such as “order 91 was paid”. A topic is a named log that stores messages in order. Each topic is split into partitions, which are separate ordered logs that let many machines share the work. A producer is a program that writes messages, and a consumer is a program that reads them. An offset is the position number of a message inside a partition, and consumers remember offsets to know where they stopped reading.
A Kafka transaction lets a producer say: “Begin. Write these messages to topic A, partition 2; these to topic B, partition 5; and record that I have finished reading up to offset 1200 of my input. Now commit.” Kafka then guarantees that all of those writes become visible to transaction-aware readers together. If the producer changes its mind or crashes, the whole bundle is thrown away as if it never happened.
Think of posting several letters in a single registered envelope. Either the envelope arrives with every letter inside, or it is returned unopened. The receiver never sees an envelope containing only half of the letters. A Kafka transaction is that sealed envelope for messages: the contents are only revealed to readers when the sender confirms the whole package is complete.
Where transactions came from
Transactions were added in Kafka version 0.11, released in 2017, together with the idempotent producer. The goal was to allow stream processing applications to produce correct results even when machines fail and messages are retried. Since then, transactions have become the base for exactly-once processing in Kafka Streams and in newer Kafka Connect features.
A Kafka transaction makes writes atomic across partitions and topics. It does not make your database, your email service or your payment gateway part of the same all-or-nothing bundle. We return to this important limit later.
2Why Kafka Needs Transactions: Delivery Guarantees
Networks fail, machines crash and messages get retried. Delivery guarantees describe what the system promises when that happens.
Whenever two programs exchange messages, something can go wrong. A message may be lost on the wire. A confirmation may be lost, so the sender does not know if the message arrived. A program may crash between two steps. Systems describe their behavior in these situations using three standard levels of promise.
| Guarantee | Meaning | Risk | Typical use |
|---|---|---|---|
| At most once | A message is delivered zero or one times | Messages can be lost | Low value telemetry where loss is fine |
| At least once | A message is delivered one or more times | Duplicates are possible | Most pipelines, paired with duplicate-tolerant consumers |
| Exactly once | The effect of each message happens once | Extra cost and complexity | Money, inventory, counts that must be right |
How duplicates and losses appear in Kafka
Producer retry
A producer sends a message, the broker stores it, but the acknowledgment is lost. The producer retries and the message is stored twice.
Crash before commit
A consumer processes a message and writes output, then crashes before saving its position. On restart it reads the same message again and repeats the work.
Partial multi-topic write
A program writes to two topics, and crashes between the two writes. One topic has the result and the other does not.
Zombie instance
A slow program is replaced by a new copy, but the old copy wakes up and keeps writing, creating a second stream of results.
Consider a courier delivering a parcel that needs a signature. If the courier leaves the parcel without waiting for a signature, it may be lost (at most once). If the courier delivers and cannot confirm, so delivers a second identical parcel, you may receive two (at least once). Exactly-once is the courier and the recipient agreeing on a tracking number so that a repeat delivery is recognized and ignored, and the whole visit is either completed or cancelled.
You often can, and for many systems it is the simpler choice. But every consumer must then do the work, get it right and store extra state. Transactions move the burden into the platform, so pipelines that read, transform and write within Kafka do not need to invent their own duplicate handling.
3The First Building Block: The Idempotent Producer
Before transactions, Kafka added a smaller feature that removes duplicates caused by producer retries.
An operation is idempotent if doing it many times gives the same result as doing it once. Pressing the “call elevator” button ten times does not summon ten elevators. An idempotent producer is a producer whose retries never create duplicate messages in a partition.
How it works
The producer gets an identity
When it starts, the broker assigns it a unique number called a producer ID.
Every batch gets a sequence number
For each partition, the producer numbers its messages 0, 1, 2 and so on, increasing by one each time.
The broker remembers the latest number
For each producer ID and partition, the broker tracks the last sequence number it accepted.
Retries are recognized
If a retried batch carries a number the broker has already stored, the broker discards the copy and simply acknowledges it again. If a number is skipped, the broker reports an error, because something was lost.
Picture a cinema that numbers every ticket sold at the booth. If a customer shows the same numbered ticket twice, the usher lets them in once and politely ignores the copy. The producer ID is the booth, the sequence number is the ticket number, and the broker is the usher.
What it covers and what it does not
Covers
- Duplicates from producer retries in one partition
- Ordering preserved despite retries
- Works for a single producer session
Does not cover
- Atomic writes across several partitions or topics
- Duplicates after the producer restarts with a new identity
- Consumer side processing repeats
In recent Kafka versions, the idempotent producer is turned on by default, and it works together with acknowledgment from all in-sync replicas. In older versions it had to be enabled explicitly, so check the setting for your version.
4Transactions Compared with Database Transactions
If you have used a database, Kafka transactions will feel familiar in spirit and different in the details.
Database transactions are often described by four properties, known as ACID: atomicity, consistency, isolation and durability. Kafka transactions provide some of these in a form that suits an append-only log.
| Property | Database meaning | Kafka equivalent |
|---|---|---|
| Atomicity | All changes happen or none do | All writes in the transaction are committed or aborted together, across partitions and topics |
| Consistency | Rules such as constraints stay true | Not enforced by Kafka; your application logic defines what a valid result is |
| Isolation | Uncommitted work is hidden from others | Consumers set to read committed hide uncommitted and aborted messages |
| Durability | Committed data survives crashes | Replication and acknowledgment settings protect committed messages |
Key differences to remember
No rollback of data
Kafka logs are append-only. Aborted messages are not removed; they stay in the log and are marked as aborted so readers skip them.
No locking of rows
Kafka does not lock messages or keys. It coordinates visibility, not access, so two transactions can run at once.
Scope is writes and offsets
A transaction bundles produced messages and consumer offsets. It does not run queries or joins.
Order stays by offset
Messages appear in the log in the order they were written, even if transactions commit in a different order.
Calling Kafka transactions “just like database transactions” leads to bad designs. There is no read-your-writes query, no cross-message locking and no multi-system commit. Treat it as a tool for atomic publishing, not as a general database.
5Architecture and Components
A transaction involves a producer, a special broker role called the coordinator, an internal log, and the partitions being written to.
flowchart LR CL["Client Requests via API Gateway and Load Balancer"] --> APP["Application with Transactional Producer"] APP --> TC["Transaction Coordinator on a Broker"] TC --> TL["Transaction Log Internal Topic"] APP --> PA["Output Topic A Partition 2"] APP --> PB["Output Topic B Partition 5"] APP --> OFF["Consumer Offsets Internal Topic"] TC --> PA TC --> PB TC --> OFF PA --> RC["Consumers with Read Committed"] PB --> RC
The components in plain words
Transactional producer
A producer configured with a stable name called a transactional ID. It begins, sends and finally commits or aborts.
Transaction coordinator
A module running inside a broker. It tracks every open transaction, decides commit or abort, and writes the final markers. Each transactional ID is assigned to one coordinator.
Transaction log
An internal replicated topic where the coordinator records the state of every transaction. If a coordinator fails, another broker reads this log and takes over.
Data partitions
The ordinary topic partitions where the messages of the transaction are written, possibly many of them across many topics.
Consumer offsets topic
The internal topic where consumer positions are saved. In a read-process-write pipeline, the new positions are written here as part of the same transaction.
Read committed consumers
Consumers that hide messages of open or aborted transactions, so only finished, successful work is visible.
The transactional ID
The transactional ID is the name you give a producer so that Kafka can recognize it across restarts. It must be unique per logical producer instance and stable over time. If a program restarts with the same transactional ID, Kafka knows it is the same actor coming back, finishes or cancels whatever transaction the previous life left open, and blocks any older copy that may still be running. Choosing this ID well is the single most important design decision when using transactions.
The transactional ID is like an employee badge number. If a cashier goes on a break and a replacement takes the same till, the manager knows exactly whose unfinished sale must be closed or cancelled. Two people using the same badge at once would be detected and stopped. A different number for every restart would leave abandoned sales lying on the counter.
The transaction coordinator chooses its home by hashing the transactional ID onto a partition of the transaction log. This spreads coordinators across brokers automatically, so adding more transactional producers does not overload one machine.
6The Life of a Transaction, Step by Step
Follow one transaction from the first call to the final commit and see what each broker and client does.
Initialize
When the producer starts, it registers its transactional ID with the coordinator. The coordinator hands back a producer ID and an epoch number, and cleans up anything the previous holder of that ID left unfinished.
Begin
The producer marks the start of a transaction. This is a local action; nothing is sent yet.
Register each new partition
The first time the producer writes to a partition inside this transaction, it tells the coordinator, so the coordinator knows every partition that will need a marker later.
Send messages
Messages are written to the data partitions immediately, flagged as belonging to an open transaction. They are stored, but hidden from read committed consumers.
Add offsets
If the program is reading and writing, the producer also sends the consumer positions to be committed with the transaction.
Commit or abort
The producer asks the coordinator to commit, or to abort. From this moment the coordinator, not the producer, drives the rest.
Markers are written
The coordinator writes a special commit or abort marker into every partition involved, and finally records that the transaction is complete.
The states of a transaction
Inside the coordinator, each transaction moves through a small set of states. It starts as empty, becomes ongoing while messages are being written, and moves to a prepare state when the producer asks to end it. A prepare state means the decision has been made and safely recorded, even though the markers are not yet in place. Once the markers are written everywhere, the state becomes complete. A transaction that runs too long is aborted by the coordinator.
| State | Meaning | Who moves it forward |
|---|---|---|
| Empty | No transaction is open for this ID | The producer, by beginning work |
| Ongoing | Messages and offsets are being added | The producer, by committing or aborting |
| Prepare commit or abort | Decision is recorded; markers are being written | The coordinator |
| Complete commit or abort | Markers are in place; the outcome is final | The coordinator |
Once the decision is written to the replicated transaction log, it can never be lost. Even if the coordinator crashes half way through writing markers, its replacement reads the log and finishes the job. This is what makes the outcome reliable.
7Two-Phase Commit and Markers
Kafka uses a classic idea, the two-phase commit, adapted for a distributed log.
The challenge is that a transaction may touch partitions on many different brokers. How do you make all of them agree, when any machine can fail at any moment? The answer is to separate the decision from the announcement.
flowchart TD A["Producer asks coordinator to commit"] --> B["Phase 1: Coordinator writes prepare commit to transaction log"] B --> C["Decision is now durable and cannot be reversed"] C --> D["Phase 2: Coordinator writes commit markers to every involved partition"] D --> E["Coordinator writes complete commit to transaction log"] E --> F["Read committed consumers can now see the messages"]
What a marker is
A control record, also called a marker, is a tiny special message written into a partition to say “the transaction with this producer ID has committed” or “has aborted”. Applications never see markers. Consumers use them, behind the scenes, to know which earlier messages to reveal or skip. That is why a topic in which a lot of transactions run holds slightly more entries than the number of messages your code wrote.
Think of a wedding planner who collects signed approvals from all the vendors before sending out invitations. First she writes in her diary, “Wedding on. Decision final.” Only after that does she call each vendor to confirm. If she drops the phone halfway through, her assistant reads the diary and finishes the calls. The diary entry is phase one, and the phone calls are phase two.
Even though markers reach different partitions at slightly different moments, readers only trust a transaction after its marker appears in a given partition. Each partition is therefore always consistent by itself, and the decision is never contradicted.
8The Consumer Side: Isolation Levels
A transaction is only useful if readers respect it. The consumer’s isolation level decides whether they do.
Every Kafka consumer has a setting called the isolation level. It has two values.
| Isolation level | What the consumer sees | Latency | Use when |
|---|---|---|---|
| Read uncommitted (default) | Every message, including those of open and aborted transactions | Lowest | Pipelines that tolerate duplicates or partial work |
| Read committed | Only non-transactional messages and messages of committed transactions | Higher; waits for commits | Any stage downstream of a transactional producer |
The last stable offset
A read committed consumer never reads beyond a position called the last stable offset. This is the offset just before the earliest transaction that is still open. Messages after that point may belong to unfinished transactions, so the consumer waits. When the open transaction commits or aborts, the position moves forward and the waiting messages are either delivered or skipped.
flowchart LR M1["Offset 10 committed"] --> M2["Offset 11 committed"] M2 --> M3["Offset 12 in open transaction"] M3 --> M4["Offset 13 committed by another producer"] M4 --> M5["Offset 14 non transactional"] M2 -.-> LSO["Last stable offset stops before offset 12"]
Notice that message 13 was committed, and yet the consumer cannot read it yet. Ordering by offset is preserved, so one slow open transaction can hold back the visibility of later messages in the same partition. This is called head-of-line blocking, and it is the main reason to keep transactions short.
Imagine a queue at a bank counter where one customer has taken a form and left to find a missing document, holding a place at the front. Customers behind them who are ready to finish cannot be served until the first customer returns or gives up. The last stable offset is the front of that queue.
Forgetting to set read committed on downstream consumers is the most frequent mistake. The producer uses transactions, but the consumer sees everything, including aborted data, and the guarantee silently disappears.
9Read, Process, Write: The Classic Pattern
The most important use of transactions is a program that reads from one topic, transforms the data, and writes to another.
Suppose a service reads payment events, calculates a fee, and writes fee events to another topic. Two things must be saved after processing a batch: the output messages, and the fact that the input has been consumed. In an ordinary program these are two separate steps. If the program crashes between them, either the output is written twice or it is never written.
flowchart TD
IN["Input Topic"] --> R["Consumer reads a batch"]
R --> P["Application processes the batch"]
P --> B["Begin transaction"]
B --> W["Write results to output topics"]
W --> O["Add consumer offsets to the transaction"]
O --> C{"Commit succeeds"}
C -->|"Yes"| OK["Results visible and input marked as consumed"]
C -->|"No"| AB["Abort: results hidden and input will be read again"]
By including the input positions in the same transaction as the output messages, Kafka makes the two effects inseparable. Either both happen, and the program moves forward, or neither happens, and the program will reread the same input and produce the same output again. Because the first attempt was hidden by the abort, the retry does not create a duplicate. This is the heart of exactly-once processing in Kafka.
Production example: payment fees
A payments company reads completed payment events and writes fee ledger entries. With a transaction around each batch, a crash mid-batch cannot leave fee entries for half of the payments, and a restart cannot double-charge the merchant.
Each transaction has fixed overhead. Grouping many input messages into one transaction spreads that cost, at the price of longer waits before results become visible. Tune the batch to your latency needs.
10Zombies and Fencing
A slow or disconnected program can wake up after it has been replaced. Kafka uses epochs to shut it out.
A zombie is an old instance of a program that the system believes is dead, but that is actually alive. It might have been paused by a long garbage collection, cut off by a network problem, or simply slow. Meanwhile, the system started a replacement. If both write to the same output, the results are duplicated or mixed. This is one of the hardest problems in distributed systems.
Kafka solves it with an epoch, a number attached to the producer ID. Each time a producer registers with a given transactional ID, the coordinator increases the epoch. Every write carries the epoch, and brokers refuse writes from an older epoch than the latest one they know. The zombie, still holding the old epoch, receives a “fenced” error and must stop.
flowchart TD A["Instance 1 starts with transactional ID X and epoch 1"] --> B["Instance 1 becomes slow or disconnected"] B --> C["Instance 2 starts with same transactional ID X"] C --> D["Coordinator raises epoch to 2 and aborts unfinished work"] D --> E["Instance 1 wakes up and tries to write with epoch 1"] E --> F["Broker rejects write: producer fenced"] F --> G["Instance 1 must shut down"]
Think of a hotel key card. When a guest loses a card and receives a new one, the hotel reprograms the lock so that the old card no longer opens the door, even if someone finds it later. The epoch is the version number of the key card, and the broker is the lock.
Why the transactional ID must be stable
Fencing works only when the old and the new instance use the same transactional ID. If each restart picked a random name, the coordinator would treat them as unrelated, and the zombie would never be blocked. For stream processing, each unit of work, such as an input partition, is usually mapped to a fixed ID, so that when work moves from one machine to another, the new owner always takes over the same identity.
Older designs needed one transactional producer per input partition to fence correctly, which used many connections. Since Kafka 2.5, the producer can pass the consumer group’s information into the transaction, and the broker fences zombies using the group’s generation instead. This lets one producer per thread do the job. It is the basis of the newer exactly-once mode in Kafka Streams.
11Timeouts and Hanging Transactions
A transaction that never finishes blocks readers. Timeouts are the safety valve.
Imagine a program begins a transaction, writes some messages, and then freezes forever. The messages are stored but never committed. Read committed consumers cannot pass the earliest open transaction, so the last stable offset stops moving. Every consumer of that partition is stuck.
Transaction timeout
The producer declares how long a transaction may stay open. If the time runs out without a commit or abort, the coordinator aborts it. The limit is capped by a broker maximum.
Fencing on restart
When the program comes back with the same transactional ID, the coordinator aborts the old transaction immediately, without waiting for the timeout.
ID expiry
A transactional ID that has been idle for a long time is eventually forgotten by the coordinator, freeing resources.
Operator tools
Recent Kafka versions include a command line tool that can list transactions and find hanging ones, so an operator can inspect and, if needed, abort them.
Choosing the timeout
Too short, and slow but healthy transactions are aborted and retried, wasting work. Too long, and a crashed producer blocks readers for a long time before the coordinator steps in. A good rule is to set the timeout to a small multiple of the longest normal transaction, and to keep normal transactions short in the first place, from a fraction of a second to a few seconds for streaming workloads.
A read committed consumer that suddenly shows growing lag while the topic still receives data often points to a hanging transaction. Check for old open transactions before adding more consumers.
12Exactly-Once in Kafka Streams
Most people meet Kafka transactions through Kafka Streams, where a single setting turns the whole machinery on.
Kafka Streams is a library for building stream processing applications. It reads from topics, transforms data, keeps state such as counts and totals, and writes results. Internally it combines every one of the pieces described so far. When you switch on exactly-once processing, Streams wraps each processing cycle in a transaction that contains three things: the output messages, the updates to the internal state changelog topics, and the input offsets.
| Guarantee setting | Behavior after a crash | Cost |
|---|---|---|
| At least once (default) | Some records may be processed again, so results may be counted twice | Lowest latency and overhead |
| Exactly once (current version) | Output, state and offsets are committed atomically; reprocessing does not double count | Slightly lower throughput; results appear at commit time |
What changes in behavior
Commit interval becomes short
With exactly-once turned on, the default commit interval drops to around a tenth of a second, so transactions are small and results appear quickly.
Consumers use read committed
Streams reads its own input in read committed mode, so it never processes data from aborted upstream work.
State is protected
Changes to the state stores are recorded in changelog topics inside the same transaction. After a failure, state is restored to match the last committed point.
Zombies are fenced
Ownership of a piece of work moves cleanly between instances, and any old owner is locked out.
Production example: counting money
A retailer counts sales per store per minute and feeds the numbers to finance reports. With at-least-once, a crash could cause a batch to be counted twice and inflate revenue. With exactly-once processing, the same crash leads to the aborted batch being redone with no visible duplication.
Kafka Streams offers an older exactly-once mode and a newer, more efficient one that relies on the improvements from Kafka 2.5. Use the newer mode when your brokers and libraries support it, and read the upgrade notes for your version.
13The Edge of the Guarantee: External Systems
Kafka can make its own writes atomic. It cannot reach into other systems and make them part of the same bundle.
Suppose a consumer reads an order, sends a confirmation email, and writes a result to Kafka inside a transaction. If the transaction aborts and is retried, the email is sent again. The email service knows nothing about Kafka transactions. The same is true for writes to a relational database, calls to a payment gateway, or updates to a search index.
| Target of the write | Covered by a Kafka transaction? | What to do |
|---|---|---|
| Kafka topics and partitions | Yes | Use a transactional producer |
| Consumer offsets in Kafka | Yes | Send offsets within the transaction |
| Relational database | No | Idempotent writes or the outbox pattern |
| Email or push notification | No | Deduplicate using a unique key |
| External payment API | No | Pass an idempotency key with each call |
Patterns for the outside world
Idempotency keys
Attach a unique identifier, such as the message key and offset, to each external call. The receiving system ignores a second request with the same identifier.
Outbox pattern
Write the business change and an “event to publish” row in one database transaction. A separate process reads the outbox and publishes to Kafka, so the database and Kafka never disagree about what happened.
Idempotent consumer
Store the identifiers of processed messages in the target system and skip any that have been seen.
Sink connectors with upserts
Write to the target by primary key, so repeating a write replaces the same row instead of creating a new one.
flowchart LR SVC["Order Service"] --> DB["Database with Orders and Outbox Table"] DB --> CDC["Outbox Reader or Change Data Capture"] CDC --> KT["Kafka Topic"] KT --> PROC["Transactional Stream Processor"] PROC --> OUT["Output Topic"] OUT --> SINK["Sink with Upserts to Warehouse"]
Your bank can guarantee that a transfer between two of its own accounts is all-or-nothing. It cannot guarantee what a shop does after you show the payment receipt. For the shop, you need another safeguard, such as a receipt number that the shop records so it never fulfils the same order twice.
Recent Kafka versions also allow certain source connectors to write exactly-once into Kafka. Support depends on the connector and the version, so check each connector’s documentation before relying on it.
14What Transactions Do Not Guarantee
Misunderstandings cause more incidents than bugs. Learn the limits before they teach you.
“Exactly once means the code runs once”
Your processing code may run more than once. What is guaranteed is that its committed effects on Kafka appear once.
“It covers my database”
Only Kafka topics and offsets are covered. Other systems need their own protection.
“Consumers see all or nothing automatically”
Only consumers set to read committed respect transactions. Default consumers see everything.
“Aborted data is deleted”
Aborted messages stay in the log until normal retention removes them. They are hidden, not erased.
“It works across clusters”
A transaction lives inside one Kafka cluster. Copying data to another cluster is a separate step with its own guarantees.
“It makes ordering global”
Ordering exists per partition. Transactions do not add ordering across partitions.
When you describe your system to others, say “exactly-once processing within Kafka” rather than “exactly-once everywhere”. The extra words prevent teams from assuming protections that do not exist.
15Configuration Cheat Sheet
A handful of settings control nearly all transactional behavior. Names are shown so that you can find them in the documentation.
| Setting | Where | Purpose |
|---|---|---|
| transactional.id | Producer | Stable name that enables transactions and fencing |
| enable.idempotence | Producer | Removes duplicates from retries; required for transactions |
| acks | Producer | Should be all so a message counts only when all in-sync replicas have it |
| transaction.timeout.ms | Producer | How long a transaction may stay open before the coordinator aborts it |
| isolation.level | Consumer | Set to read_committed to hide open and aborted data |
| enable.auto.commit | Consumer | Turn off when offsets are committed through the transaction |
| transaction.max.timeout.ms | Broker | Upper limit of the timeout a producer may request |
| min.insync.replicas | Broker or topic | Minimum copies that must acknowledge a write |
| transaction.state.log.replication.factor | Broker | Copies of the transaction log; use three in production |
| processing.guarantee | Kafka Streams | Switches Streams to exactly-once processing |
Defaults have changed between versions. Always read the documentation for the exact version you run before assuming a value.
16Producer Reliability Settings Explained
Transactions sit on top of the ordinary producer safety settings. If those are weak, the whole structure is weak.
A producer sends messages to a partition leader, which copies them to follower replicas. The producer chooses how much confirmation it wants before it considers a message safely stored. This choice is controlled by a setting called acks. With acks set to zero, the producer does not wait for any reply and may lose messages silently. With acks set to one, the leader confirms once it has written the message, but a crash of the leader right after can lose it before any follower copies it. With acks set to all, the leader confirms only after every in-sync replica has the message, which is the safe choice and the one transactions require.
| Setting value | Wait for | Risk |
|---|---|---|
| acks zero | Nothing | Messages lost without any error |
| acks one | The leader only | Loss if the leader fails before followers copy |
| acks all | All in-sync replicas | Slightly higher latency; safest |
Retries and ordering
When a send fails for a temporary reason, the producer retries. Without idempotence, a retry can reorder messages: batch one fails, batch two succeeds, and then batch one is retried and lands after batch two. With the idempotent producer, sequence numbers let the broker put batches in the right order and drop duplicates, even with several requests in flight at once. The recommended limit of in-flight requests with idempotence is five or fewer.
The minimum in-sync replicas rule
Acks all means “all replicas currently in sync”, which could shrink to just the leader if followers fall behind. The topic setting min.insync.replicas sets a floor: if fewer than the required number of replicas are in sync, the leader refuses the write instead of accepting it with weak protection. The common production pairing is three replicas with a minimum of two, so that one broker can fail without losing data or stopping writes.
Think of signing an important contract. Acks zero is shouting the terms across a room and hoping someone heard. Acks one is one witness nodding. Acks all is every required witness signing the page. The minimum in-sync rule says the contract is not valid unless at least two witnesses are actually present.
The idempotent producer expects acks all and retries enabled. Settings that contradict these can cause the producer to refuse to start, which is a good thing, because a half-safe configuration is worse than a failure.
17How Consumers Filter Aborted Data
Aborted messages stay in the log. The consumer library quietly removes them before your code sees anything.
When a read committed consumer fetches data, the broker sends the raw messages along with a short list of aborted transactions in the range that was fetched. Each entry names the producer ID that aborted and the offset where its transaction began. Brokers keep an index file for each log segment to answer this quickly, so they do not need to scan every message.
Fetch
The consumer asks for messages starting at its current offset, up to the last stable offset.
Receive data with the aborted list
The response holds the messages plus the list of producer IDs whose transactions were aborted in that range.
Track aborted producers
As the consumer reads forward and reaches the start offset of an aborted transaction, it starts skipping messages from that producer ID.
Stop skipping at the marker
When the consumer reaches the abort marker for that producer, it stops skipping and continues normally.
Deliver clean data
Your application receives only messages that are safe to use.
Because this filtering happens inside the client library, your code stays simple. The cost is a little extra memory and bookkeeping in the consumer, which grows when many transactions are open at once. A workload with a very high abort rate makes consumers do more filtering, so abort rates should stay low in healthy systems.
Picture a library returns desk where a clerk receives a cart of books together with a slip listing “these three books were returned damaged and must not go back on the shelves”. The clerk quietly sets those aside while shelving the rest. Readers only ever see the shelved books. The slip is the aborted transactions list.
18Offsets, Markers and Compaction
Markers use up offsets and aborted data lingers. Both surprise people who look closely at their topics.
Gaps in offsets
Every commit or abort marker takes one offset in each partition it is written to. If a partition holds ten messages and one transaction marker, the offsets run from zero to ten, with the marker at the end. A consumer never receives the marker, so it sees a gap in offsets where the marker sits. Tools that compute lag by subtracting offsets, and any code that assumes offsets increase by exactly one, must allow for these gaps. A partition full of very small transactions has many gaps.
Retention and compaction
Aborted messages are removed by normal cleanup. Under time or size based retention, whole old segments are deleted, including their aborted messages and markers. Under log compaction, where Kafka keeps the latest value for each key, the cleaner also removes aborted data during its work and eventually cleans up the markers themselves. Until that cleanup happens, aborted data takes disk space, which is another reason to keep abort rates low.
Offset gaps
Lag numbers can look slightly off. Do not treat a small permanent difference as an error.
Extra disk use
Markers and aborted messages occupy space until retention or compaction removes them.
Tools that count messages
Counting by offset difference overestimates the number of visible messages when markers are present.
Compacted topics
The latest committed value per key is what remains, so state topics stay correct after cleanup.
When comparing message counts between two topics, count the messages that consumers actually receive, not the difference between the first and last offsets.
19Migrating an Existing Pipeline to Exactly-Once
Most teams do not start with transactions. They add them to a pipeline that already runs. Do it in a careful order.
Confirm the cluster supports it
Check broker and client versions, the replication settings of the transaction log, and the minimum in-sync replicas. Fix weak settings before anything else.
Move the downstream readers first
Switch every consumer of the topics you will write transactionally to read committed. This is harmless while writes are still non-transactional, and it protects readers in advance.
Enable the idempotent producer
This alone removes duplicates from retries and is a safe first improvement.
Choose transactional IDs
Design stable names tied to units of work, and grant ACLs for them.
Turn on transactions in one service
Start with the least critical pipeline. Watch commit latency, abort rate and lag.
Test failure on purpose
Kill instances and brokers, and verify that outputs are correct. Fix what you find.
Roll out gradually
Move the remaining services one by one, keeping dashboards for open transactions and fencing errors.
Production example: a careful first step
A retailer moved its order pipeline to exactly-once by first switching all consumers to read committed on a quiet Tuesday, then enabling transactions on the inventory service alone. Only after a week of clean metrics and a planned broker failure test did it enable the remaining services.
During a migration some services write transactionally and others do not. Consumers set to read committed handle both kinds of writers correctly, so the reader-first order is what keeps the mixed period safe.
20Sizing the Transaction Log
The transaction log is small compared with your data topics, but it sits on the critical path of every commit.
The log is an internal, compacted, replicated topic with a fixed number of partitions, fifty by default. Each transactional ID is mapped to one of these partitions by hashing, and the broker that leads that partition is the coordinator for the ID. The number of partitions is chosen when the cluster first creates the log, and it should not be changed later, because changing it would remap IDs to different coordinators.
| Item | Guidance | Why |
|---|---|---|
| Partitions of the log | Leave at the default unless you have measured a need | Fifty spreads coordinators well; changing later is risky |
| Replication factor | Three | Coordinator failover depends on replicas |
| Minimum in-sync replicas | Two | Prevents accepting decisions with weak protection |
| Idle ID expiry | Keep the default of about seven days, or set to match your idle patterns | Frees state for IDs that are no longer used |
| Number of transactional IDs | Plan for units of work times environments | Each ID holds a small amount of state |
| Disk speed | Use fast storage on brokers | Every state change is a write on the commit path |
A hotel front desk holds a small ledger of which guest has which room key. The ledger is tiny compared with the hotel, but if it is lost or slow, nobody can check in. The transaction log is that ledger: small, and vital.
Configure the log’s replication and minimum in-sync settings before the cluster receives its first transactional request, because the log is created lazily at that moment using the settings of that time.
21Failure Scenarios and Recovery
The best way to trust a transaction system is to know exactly what it does in every bad situation.
| Failure | What Kafka does | What you should see |
|---|---|---|
| Producer crashes before commit | Coordinator aborts the open transaction after the timeout, or immediately when the same ID returns | Aborted messages hidden; input reread and reprocessed |
| Producer crashes after asking to commit | Coordinator completes the commit on its own | Messages become visible together |
| Coordinator broker fails | A replica takes over and reads the transaction log | Short pause, then normal completion |
| Partition leader fails during a transaction | New leader elected; the producer may retry the batch | Retries with sequence numbers cause no duplicates |
| Network partition isolates the producer | Timeout aborts the transaction; a replacement fences the old producer | Old producer receives a fenced error when it reconnects |
| Consumer crashes after processing | Offsets not committed; on restart, the same input is read again | Output from the first attempt was aborted, so no duplicates |
| Log segments deleted by retention before reading | Consumer offset out of range | Follow your offset reset policy; a data gap is possible |
Reading the table
Notice how each row has the same shape: something goes wrong, Kafka relies on a durable record such as the transaction log or the epoch, and the outcome is either a hidden abort followed by a clean retry, or a completed commit. No row leaves half of a transaction visible. That consistent shape is the entire purpose of the design.
Every row of this table can be turned into a test: cause the failure on purpose in a staging cluster and confirm that the observed result matches the last column.
22A Decision Guide: Do You Need Transactions?
Transactions are valuable, not free. Use this guide to decide pipeline by pipeline.
| Question | If yes | If no |
|---|---|---|
| Is a duplicate result harmful, such as a double charge or inflated count? | Use transactions or idempotent sinks | At-least-once is likely enough |
| Does one input produce writes to several topics that must agree? | Use transactions for atomic publishing | A plain idempotent producer may do |
| Does the pipeline keep running totals or other state? | Use exactly-once in Kafka Streams | Simple stateless steps rarely need it |
| Are the results consumed only by Kafka readers you control? | Read committed can be enforced everywhere | Outside readers need extra protection |
| Can the team run and monitor the extra moving parts? | Proceed with runbooks and alerts | Start with simpler duplicate handling |
| Is very low latency more important than exactness? | Consider at-least-once | Transactions fit well |
If most answers point toward transactions, adopt them for that pipeline and keep the rest simple. There is no reward for turning them on everywhere. A focused use on the few paths where exactness matters gives the benefits while keeping overhead and operational effort small.
Pick the single pipeline where a duplicate would hurt most, make it exactly-once, learn from running it, and only then decide about the next one.
23Advantages, Disadvantages and Trade-offs
Transactions buy correctness and pay for it with some speed and some complexity. Most teams should use them only where the trade is justified.
Advantages
- Atomic writes across many partitions and topics
- Duplicates from retries and crashes are avoided within Kafka
- Input offsets and outputs are committed as one unit
- Zombie producers are fenced automatically
- Downstream readers can rely on committed data only
- Kafka Streams offers all this through a single setting
Disadvantages
- Extra latency, since results are visible only after commit
- Lower throughput than plain at-least-once, especially with tiny transactions
- Open transactions can block readers
- Guarantee stops at the edge of Kafka
- More settings to understand and operate
- Aborted data still occupies storage until retention removes it
The main trade-offs
Latency versus overhead
Small transactions commit quickly but pay the fixed cost often. Large transactions are efficient but delay visibility.
Correctness versus simplicity
Exactly-once removes duplicate handling from application code but adds operational concepts to learn.
Timeout length versus safety
Short timeouts recover quickly from crashes but abort slow healthy work. Long timeouts are tolerant but risk long blockage.
Read committed versus freshness
Hiding uncommitted data protects consumers and makes them wait for the last stable offset.
Ask: “What is the cost of a duplicate or a lost result?” If the cost is small, at-least-once with idempotent consumers is often simpler. If the cost is money, legal risk or customer trust, transactions earn their place.
24Performance and Scalability
Transactions add work, but a well-tuned system can handle very high volumes.
The extra cost of a transaction comes from a few fixed steps: registering partitions with the coordinator, writing state changes to the transaction log, and writing markers to each partition. These steps are the same whether a transaction holds ten messages or ten thousand. That is why the size of the transaction matters so much for efficiency.
| Factor | Effect | How to tune |
|---|---|---|
| Messages per transaction | More messages spread the fixed cost | Batch reasonably, guided by latency needs |
| Partitions per transaction | Each partition needs registration and a marker | Avoid touching more partitions than necessary |
| Number of transactional producers | Each has its own state at the coordinator | Use one producer per thread where supported |
| Commit interval in Streams | Short interval means more, smaller transactions | Raise it if latency allows |
| Read committed reads | Consumers must filter aborted data | Keep aborts rare |
| Transaction log load | Every state change is a write | Spread across enough log partitions; use fast disks |
Sending parcels by courier costs a pickup fee for each visit. Sending one parcel per visit is expensive per item. Bundling fifty parcels in one visit costs the same pickup fee, spread thin. The trade-off is that the earliest parcel waits until the courier arrives. A transaction commit is the courier visit.
Production example: high volume stream with modest overhead
Confluent and other teams have published benchmarks showing that when transactions carry a sensible number of messages, the drop in throughput compared with at-least-once is modest, often only a few percent. Very small transactions, for example one message each, show much larger overhead. Treat published numbers as guidance and measure your own workload.
Wrapping every single message in its own transaction is the most common performance mistake. Group messages into batches that match your latency target.
25High Availability and Reliability
A transaction system must survive broker failures, network splits and application crashes without changing its answer.
flowchart TD F["Coordinator broker fails during commit"] --> L["New coordinator elected from replicas of the transaction log"] L --> R["New coordinator reads log and finds prepare commit"] R --> M["It finishes writing commit markers"] M --> D["Transaction completes with the original decision"]
Three copies of everything
Set the transaction log and data topics to three replicas, with at least two required to acknowledge each write.
Wait for all in-sync replicas
Producers should require acknowledgment from all in-sync replicas, so a committed message survives one broker failure.
Coordinator failover
Because the log is replicated, another broker can take over as coordinator and finish any transaction in progress.
Retry with a fresh transaction
On most errors, the application aborts the transaction and retries the batch. On a fencing error, it must shut down.
Handling errors correctly
Errors fall into three groups. Retriable errors, such as a temporary network problem, are handled by the client library. Abortable errors mean the current transaction cannot complete, so the application aborts it and starts over with the same input. Fatal errors, such as being fenced by a newer instance, mean the producer must close and stop writing. Confusing these groups, for example retrying after a fatal error, can create the very duplicates transactions try to prevent.
Production example: planned broker maintenance
A financial platform restarts brokers one at a time for upgrades. Because transaction logs and data topics are fully replicated, producers see brief pauses, some transactions abort and retry, and consumers reading committed data see no gaps or duplicates.
26Security
Transactions add new resources that need protection. Treat the transactional ID like a credential.
Authenticate producers
Require each application to prove who it is, for example with TLS certificates or SASL, so that only known programs can start transactions.
ACLs on transactional IDs
Kafka has an access control resource for transactional IDs. Grant each application permission only to its own IDs, so that one service cannot fence or abort another.
Least privilege on data
Give write access only to the topics an application must produce to, and read access only to those it must consume.
Encrypt connections
Use TLS between clients and brokers and between brokers, so that messages and coordination traffic cannot be read or altered.
Guard the internal logs
The transaction log and offsets topic are internal. Normal applications should not read or write them directly.
Predictable but private IDs
A structured naming scheme helps operations. Combine it with ACLs so that guessing a name does not grant power over it.
Anyone allowed to use a transactional ID can start a new session with it and thereby fence, and stop, the legitimate producer. Protect transactional IDs as carefully as you protect write access to the data itself.
27Monitoring, Logging and Metrics
A few signals reveal almost every transactional problem before customers notice.
| Signal | What it tells you | Typical action |
|---|---|---|
| Consumer lag of read committed groups | Whether readers are stuck behind an open transaction | Look for hanging transactions before scaling out |
| Age of the oldest open transaction | Very old open transactions block the last stable offset | Investigate the producer; consider aborting |
| Abort rate | How often transactions fail and are retried | Check timeouts, errors and downstream failures |
| Fencing errors | Instances competing for one transactional ID | Check for duplicate deployments or unstable membership |
| Commit latency | Time from commit request to completion | Look at broker load and replication |
| Transaction log health | Under-replicated partitions in the internal log | Treat as urgent; the commit path depends on it |
| Producer batch size and transaction size | Efficiency of each transaction | Increase batch size if transactions are tiny |
Log the transactional ID, the epoch and the outcome of each transaction at a low level of detail, so that when something looks wrong, you can trace a specific run. Add an alert when a transaction stays open for longer than a few times your normal duration, since this is the earliest warning of trouble.
Keep a runbook page for hanging transactions: how to find them, how to decide whether to wait or abort, and who to notify. In an incident, a written procedure is worth far more than memory.
28Worked Example: A Money Transfer Pipeline
Walk through a realistic service, first without transactions and then with them, and see exactly what changes.
A bank publishes transfer requests to a topic. A service reads each request and writes two events: a debit event to a topic named “debits” and a credit event to a topic named “credits”. Each request should produce exactly one debit and one credit.
Without transactions
The service reads request 55, writes the debit event, and then its machine loses power before it writes the credit. On restart, the service has not saved its position, so it reads request 55 again. It writes the debit again, and then the credit. The account holder is debited twice and credited once. Alternatively, if the service saved its position before writing, a crash would lose the transfer entirely.
With transactions
Read request 55
The service polls the input with the read committed setting.
Begin a transaction
The transactional producer starts a new transaction under its stable ID.
Write debit and credit
Both events go to their partitions, flagged as part of the open transaction and hidden from readers.
Add the input offset
The new position, just after request 55, joins the transaction.
Commit
The coordinator records the decision and writes commit markers on the debits partition, the credits partition and the offsets topic.
Now consider the same power failure. If it happens before the commit, the coordinator aborts the transaction after a timeout or as soon as the service restarts with the same ID. The debit and credit are hidden, the input position was never advanced, and the service reads request 55 again and repeats the work. Readers of the debit and credit topics never saw the aborted events. If the failure happens after the commit decision is recorded, the coordinator finishes writing the markers. Either way, the account holder sees exactly one debit and one credit.
| Crash moment | Without transactions | With transactions |
|---|---|---|
| After debit, before credit | Debit exists alone; retry may duplicate it | Both events hidden and later redone |
| After both writes, before saving position | Request is reprocessed; both events duplicated | Aborted; only one pair ever becomes visible |
| During commit | Not applicable | New coordinator completes the commit |
| Old instance wakes up later | Old instance writes duplicates | Old instance is fenced and stopped |
The guarantee only holds if the systems reading the debits and credits topics use read committed. A reader in the default mode would still see the hidden events from aborted attempts.
29Testing Transactional Applications
Failures are rare in production, so you must create them on purpose in testing.
Happy path
Send a known set of inputs and check that outputs appear exactly once for a read committed reader.
Kill the application mid-transaction
Stop the process after writing but before committing, restart it, and confirm that no duplicates or partial results are visible.
Kill a broker
Restart the broker that acts as coordinator while a transaction is in flight, and verify the outcome is still consistent.
Create a zombie
Pause an instance long enough to be replaced, then resume it, and confirm it is fenced and shuts down.
Check the readers
Read the output with both isolation levels and compare the results. The difference shows what aborted data would have leaked.
Soak and load test
Run for hours at production volume and watch commit latency, abort rate and lag.
Kafka Streams also ships with a test driver that runs a topology in memory, which is excellent for checking logic. However, the driver does not reproduce real broker failures, so it complements, but cannot replace, tests against a real cluster with deliberate crashes.
A fire drill does not prove that a building will never burn. It proves that people know the exits and that the alarms work when they are needed. Crash tests for transactions serve the same purpose: they show that the recovery path works before a real failure demands it.
30Design Patterns and Anti-patterns
A few proven shapes solve most needs. A few tempting shortcuts cause most of the outages.
Patterns that work
Consume, transform, produce
Wrap each batch of input in one transaction with its outputs and offsets. This is the standard exactly-once loop.
Transactional outbox
Publish database changes to Kafka via an outbox table, so the database and the topic never diverge.
Idempotent sinks
End every pipeline with a sink that writes by primary key, so any leftover duplicates collapse into one row.
Stable IDs by work unit
Derive the transactional ID from the unit of work, not from the machine, so ownership can move without confusion.
Anti-patterns to avoid
What people do
Use a random transactional ID on every start.
Why it hurts
The coordinator cannot recognize the returning program, so unfinished transactions linger and zombies are never fenced.
Do this instead
Use a stable ID tied to the unit of work.
What people do
Produce transactionally but leave downstream consumers on the default isolation level.
Why it hurts
Readers still see aborted and open data, so the guarantee is lost without any error message.
Do this instead
Set read committed on every consumer that reads transactional output.
What people do
Call an external API inside the transaction and assume a retry is safe.
Why it hurts
An aborted and retried transaction repeats the external call, sending a second email or charging a card twice.
Do this instead
Use idempotency keys, or the outbox pattern, for anything outside Kafka.
What people do
Keep transactions open for a long time, for example while waiting on a slow service.
Why it hurts
The last stable offset stalls and every read committed consumer of those partitions waits.
Do this instead
Do slow work before beginning the transaction, and keep the transaction itself short.
31Best Practices and Common Mistakes
A short checklist gathered from production systems. Follow it and you avoid most transaction pain.
Decide whether you need transactions
Use them where duplicates or partial results are costly. Otherwise, prefer simple at-least-once plus idempotent consumers.
Use stable transactional IDs
One ID per unit of work, the same across restarts.
Set read committed downstream
Every consumer of transactional output must use it.
Keep transactions short but not tiny
Batch enough messages to spread the fixed cost, and stay well below the timeout.
Replicate everything
Three replicas, acknowledgment from all in-sync replicas, and a sensible minimum for in-sync copies.
Handle errors by type
Retry retriable errors, abort and retry abortable ones, and stop on fatal ones such as fencing.
Protect the edges
Use idempotency keys or the outbox pattern for every system outside Kafka.
Mistakes seen again and again
Forgetting read committed
The most frequent cause of “exactly-once did not work” reports.
Auto-committing offsets
Leaving automatic offset commits on lets the consumer save its position outside the transaction.
One transaction per message
Fixed overhead dominates and throughput collapses.
Duplicate deployments
Two copies running with the same ID fence each other in a loop.
Weak replication of the log
A transaction log with one replica makes the whole feature fragile.
Assuming external effects are covered
Databases, email and payments need their own protection.
32Transactions Versus Other Approaches
Kafka transactions are one tool among several. Knowing the alternatives helps you pick the simplest one that works.
| Approach | How it works | Strength | Weakness |
|---|---|---|---|
| Kafka transactions | Atomic writes and offsets inside one cluster | Exactly-once for Kafka to Kafka pipelines | Does not extend to outside systems |
| Idempotent consumers | Consumer remembers processed IDs and skips repeats | Works with any target system | Every consumer must implement it and store state |
| Outbox pattern | Database change and event stored together, published later | Keeps database and Kafka aligned | Extra table and a publishing process |
| Saga pattern | A chain of local steps with compensating actions to undo them | Works across many independent services | Complex logic; states are visible mid-way |
| Distributed two-phase commit | A coordinator locks and commits across several databases | Strong atomicity across systems | Slow, blocking and rarely supported with Kafka |
Booking a holiday needs a flight, a hotel and a car. A saga is like booking each separately and cancelling the earlier ones if a later one fails. Kafka transactions are like a travel agent who can guarantee that all the bookings made through the agent’s own office succeed or fail together, but who cannot control other companies’ systems.
Real systems often mix approaches: transactions inside the Kafka pipeline, the outbox pattern at the entrance, and idempotent sinks at the exit. Each covers the part of the path where it is strongest.
33Real-World and Industry Examples
Transactions appear wherever a wrong or duplicated result costs money or trust.
Kafka began at LinkedIn to move activity data, and transactions were added to support stream processing that businesses could trust with critical numbers. Public talks and articles from many companies describe patterns like the ones below. The internal designs of specific companies change over time, so treat these as illustrations of the technique rather than exact blueprints.
Banking: ledgers and transfers
Banks publish debit and credit events and rely on atomic writes so that an outage cannot leave half a transfer visible. Downstream reporting and fraud systems read only committed events.
Payments: fee and settlement calculation
Payment platforms compute fees, refunds and settlements from streams of payment events. Exactly-once processing keeps merchant balances correct even when machines restart during heavy traffic.
E-commerce: inventory and orders
Online retailers reserve stock when orders arrive. Writing the reservation and the order status together prevents the same item from being promised twice after a retry.
Ride-hailing: trip billing
Ride platforms turn trip events into fares. A transaction around each calculation stops a restart from charging a rider twice for the same trip.
Advertising: spend tracking
Ad platforms track spend against budgets in near real time. Kafka Streams with exactly-once processing keeps running totals from being inflated by reprocessing after failures. Pinterest has publicly described using Kafka Streams for budget related processing.
Data platforms: enrichment pipelines
Pipelines that join, clean and enrich events before loading a warehouse use transactions so that aborted batches never reach the warehouse loader.
The common thread is money or counts that feed decisions. Where a duplicate is harmless, teams usually skip transactions and keep the pipeline simple.
34Glossary of Terms
A quick reference for the vocabulary used throughout this guide.
| Term | Plain meaning |
|---|---|
| Transaction | A group of writes that succeed or fail together |
| Idempotent | Safe to repeat; repeating gives the same result as doing it once |
| Producer ID | A number the broker assigns to identify a producer session |
| Sequence number | A per-partition counter used to detect duplicate or missing batches |
| Transactional ID | A stable name for a producer that enables transactions and fencing |
| Epoch | A version number that increases each time a transactional ID is re-registered |
| Transaction coordinator | The broker module that manages the state of transactions |
| Transaction log | The internal replicated topic where transaction states are stored |
| Control record or marker | A hidden message that marks a transaction as committed or aborted |
| Isolation level | The consumer setting that decides whether uncommitted data is visible |
| Last stable offset | The position before the earliest open transaction; the limit for read committed readers |
| Zombie | An old instance that keeps running after being replaced |
| Fencing | Blocking a zombie so that it cannot write any more |
| Exactly-once processing | Each input affects the output once, within Kafka |
| Outbox pattern | Storing an event in the database with the change, then publishing it separately |
35Frequently Asked Questions
Quick answers to the questions that come up most when teams first use Kafka transactions.
36Summary and Key Takeaways
Kafka transactions make a bundle of writes succeed or fail as one, so that pipelines can stay correct through failures.
Key Takeaways
- A transaction is an atomic bundle. Writes to many partitions and topics, plus consumer offsets, are committed or aborted together, and only inside one Kafka cluster.
- Idempotence is the foundation. Producer IDs and sequence numbers remove duplicates from retries, and transactions build on that with a coordinator, a replicated log and commit or abort markers.
- Readers must cooperate. Only consumers set to read committed hide open and aborted data, and they read up to the last stable offset, so open transactions can hold them back.
- Stable transactional IDs and epochs fence zombies. The newest producer wins, and older copies receive an error and must stop.
- Exactly-once processing means committed effects appear once. Your code may still run more than once, and everything outside Kafka needs idempotency keys, the outbox pattern or idempotent sinks.
- Tune for short, sensible transactions. Batch enough to spread the fixed cost, avoid one transaction per message, keep timeouts realistic, and never hold a transaction open while waiting on slow work.
- Operate it with care. Replicate the transaction log, protect transactional IDs with ACLs, alert on old open transactions and fencing errors, and rehearse failures before they happen.