Transactions
The idea in one sentence
Section titled “The idea in one sentence”A Kafka transaction lets a producer write to multiple partitions (and commit consumer offsets) atomically — everything in the transaction becomes visible together on commit, or none of it does on abort.
Configuring a transactional producer
Section titled “Configuring a transactional producer”Transactions build on the idempotent producer. You add a transactionalId — a stable identifier that lets Kafka fence out zombie instances of the same logical producer — along with idempotent: true and maxInFlightRequests: 1. Connecting the producer registers the transactional id; unlike the Java client, KafkaJS has no separate initTransactions() call.
import { Kafka } from 'kafkajs'
const kafka = new Kafka({ clientId: 'payments', brokers: ['localhost:9092'] })
// A transactional producer: a stable transactionalId is the key ingredientconst producer = kafka.producer({ transactionalId: 'payments-tx-1', // stable id enables transactions + zombie fencing idempotent: true, // required foundation maxInFlightRequests: 1,})
await producer.connect() // connecting registers the transactional idThe transaction lifecycle
Section titled “The transaction lifecycle”Start a unit of work with producer.transaction(). Any sends on the returned txn are buffered into the transaction; txn.commit() makes them all visible atomically, and txn.abort() discards them:
const txn = await producer.transaction() // begintry { await txn.send({ topic: 'accounts', messages: [{ key: debitKey, value: debit }] }) await txn.send({ topic: 'accounts', messages: [{ key: creditKey, value: credit }] }) await txn.commit() // both writes appear together, or neither} catch (e) { await txn.abort() // roll back the whole unit}Consumers must read committed
Section titled “Consumers must read committed”Transactions only matter if readers ignore aborted and in-flight data. Set the consumer to readUncommitted: false — read_committed, and the KafkaJS default — so it never sees records from aborted or open transactions:
const consumer = kafka.consumer({ groupId: 'ledger', readUncommitted: false, // read_committed: skip aborted and in-flight records})flowchart LR init["connect() once (registers txn id)"] --> begin["producer.transaction()"] begin --> s1["txn.send to accounts-0"] begin --> s2["txn.send to accounts-2"] s1 --> commit["txn.commit(): all visible together"] s2 --> commit begin -.->|on error| abort["txn.abort(): nothing visible"]
KIP-890 in Kafka 4.x
Section titled “KIP-890 in Kafka 4.x”Kafka 4.x finalizes KIP-890, Transactions Server-Side Defense, which strengthens the transactional protocol against edge cases like hanging transactions and zombie producers. When the 4.0 upgrade is finalized the new, hardened protocol is enabled — so transactional pipelines on Kafka 4.x are more robust than on older versions with no code change on your side.