Skip to content

Transactions

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.

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 ingredient
const 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 id

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() // begin
try {
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
}

Transactions only matter if readers ignore aborted and in-flight data. Set the consumer to readUncommitted: falseread_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"]
A transaction commits multiple writes atomically

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.

What does a Kafka transaction guarantee?
What is the role of `transactionalId`?
What must a consumer set to correctly read transactional data?
What does KIP-890 do in Kafka 4.x?