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?
answer
- EOS = transactional producer + read_committed consumer
- sendOffsetsToTransaction: outputs + offsets atomic
- default read_uncommitted leaks aborted records downstream
- silent bug: only fails under aborts
- Streams exactly_once_v2 sets it automatically
basics
~20 sExactly-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 sKafka 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# 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=allgo deeper
Know exactly-once needs read_committed so consumers don't see rolled-back records.
Pair transactional producers with read_committed consumers and disabled auto-commit.
Explain sendOffsetsToTransaction, the full config set, and why read_uncommitted breaks EOS under aborts.
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.