skip to content

When building an exactly-once read-process-write pipeline on Kafka, why is isolation.level=read_committed required on the consuming side, and what breaks if you forget it?

level: principalimportance: should knowfreq 30%

answer

  1. EOS = transactional producer + read_committed consumer
  2. sendOffsetsToTransaction: outputs + offsets atomic
  3. default read_uncommitted leaks aborted records downstream
  4. silent bug: only fails under aborts
  5. Streams exactly_once_v2 sets it automatically

basics

~20 s

Exactly-once needs downstream stages to never act on rolled-back data. read_committed ensures the consumer only sees committed records. With the default read_uncommitted, the consumer processes aborted/uncommitted records, so a rolled-back transaction still leaks downstream, breaking exactly-once.

solid answer

~40 s

Kafka exactly-once (EOS) chains transactional producers with read_committed consumers. A read-process-write stage consumes input, processes it, and produces output plus its input offsets inside one transaction via sendOffsetsToTransaction, then commits atomically. The guarantee only holds if the *next* stage refuses to observe data from aborted or in-flight transactions — that is exactly what isolation.level=read_committed provides via LSO gating and aborted-record filtering. If you leave the default read_uncommitted, a downstream consumer will read records from a transaction that later aborts (or hasn't committed), process them, and emit derived effects — so a rolled-back unit of work is no longer atomic from the consumer's view; you get duplicates or phantom outputs and lose exactly-once. Kafka Streams sets read_committed automatically under processing.guarantee=exactly_once_v2; hand-rolled pipelines must set it explicitly on every consuming stage.

code

properties · 8 lines
properties
# Consuming side of an EOS read-process-write stage
isolation.level=read_committed
enable.auto.commit=false

# Producing side
enable.idempotence=true
transactional.id=order-enricher-1
acks=all

go deeper

for a junior

Know exactly-once needs read_committed so consumers don't see rolled-back records.

for a middle

Pair transactional producers with read_committed consumers and disabled auto-commit.

for a senior

Explain sendOffsetsToTransaction, the full config set, and why read_uncommitted breaks EOS under aborts.

for a principal

Reason about EOS as a producer+consumer composition, the silent-failure mode, Streams exactly_once_v2, and the latency trade-offs.

## What exactly-once on Kafka actually is Kafka's exactly-once semantics (EOS) for stream processing is a **chain** of two cooperating mechanisms: 1. **Transactional producers** (a `transactional.id` + the idempotent producer) that write output records and the consumer's input offsets **atomically**. The standard read-process-write loop is: poll input, process, `producer.sendOffsetsToTransaction(offsets, consumerGroupMetadata)` to record how far the input was consumed *inside the same transaction* as the outputs, then `commitTransaction()`. Commit makes the outputs and the offset advance visible together; abort makes neither visible. 2. **read_committed consumers** downstream, which by LSO gating + aborted-transaction filtering only ever observe committed outputs. These two halves are both necessary. The producer side gives *atomic write of outputs + offsets*; the consumer side gives *no observation of non-committed data*. EOS is the composition. ## Why read_committed is mandatory on the consuming side Suppose stage A transactionally produces to topic T but a particular transaction **aborts** (crash, validation failure, explicit rollback). The records were physically written to T before the abort. If stage B reads T with the default **read_uncommitted**, it will: - read those aborted records, - process them, - and produce derived effects (writes, side effects, its own outputs). Now a unit of work that A rolled back has nonetheless propagated downstream. The atomicity A paid for is undone one hop later. You observe **duplicates or phantom records** — precisely the failure EOS is supposed to eliminate. The same happens for *uncommitted-but-still-open* transactions: B might process records that are later aborted. So read_committed is not an optimization; it is a **correctness requirement** for the consuming end of every EOS hop. ## What you must set, end to end - Producer: `enable.idempotence=true` (implied with a transactional.id), a unique-and-stable `transactional.id`, `acks=all`. Use `beginTransaction`/`sendOffsetsToTransaction`/`commitTransaction`. - Consumer: **`isolation.level=read_committed`**, and `enable.auto.commit=false` (offsets are committed through the producer's transaction, not by the consumer auto-commit). - Broker: `transaction.state.log.replication.factor` and `transaction.state.log.min.isr` sized for durability (commonly 3/2 in production). ## Kafka Streams does this for you With **`processing.guarantee=exactly_once_v2`**, Kafka Streams automatically configures transactional producers, disables consumer auto-commit, and sets `isolation.level=read_committed` on its internal consumers. Hand-rolled consumer/producer pipelines get none of this automatically — forgetting read_committed is a classic silent EOS bug because everything *appears* to work until a transaction aborts under failure. ## The subtle failure mode The bug is silent in the happy path: with no aborts, read_uncommitted and read_committed deliver the same records, tests pass, demos work. It only manifests under the exact failure conditions EOS exists to handle — producer crashes, rollbacks, rebalances during a transaction — making it a dangerous omission to catch only in production. ## Costs you accept In exchange you accept LSO-gating latency (records visible at commit, not produce) and head-of-line blocking from long transactions — so EOS pipelines favor short, frequent transactions and tuned `transaction.timeout.ms`.

  • How do input offsets get committed in an EOS read-process-write loop?
    Via producer.sendOffsetsToTransaction(offsets, consumerGroupMetadata) inside the transaction, with consumer auto-commit disabled. The offset advance commits atomically with the outputs.
  • Why is forgetting read_committed a dangerous, easy-to-miss bug?
    In the happy path with no aborts, read_uncommitted and read_committed return the same records, so tests and demos pass. It only breaks when a transaction actually aborts — under failure — leaking rolled-back data downstream.
  • Does Kafka Streams require setting isolation.level manually?
    No. processing.guarantee=exactly_once_v2 configures transactional producers, disables auto-commit, and sets isolation.level=read_committed on its internal consumers automatically.

saying these in an interview costs you the question

  • Treating read_committed as a performance tuning, not a correctness requirement for EOS.
  • Believing transactional producers alone give exactly-once without read_committed consumers.
  • Leaving enable.auto.commit=true in an EOS loop (offsets must go through the transaction).
  • Assuming the happy-path test coverage proves EOS correctness.

context