skip to content

What does setting a Kafka consumer's isolation.level to read_committed do, and why does it matter when producers use transactions?

level: juniorimportance: must knowfreq 45%

answer

  1. default = read_uncommitted (dirty reads)
  2. read_committed hides aborted records
  3. won't read past Last Stable Offset (LSO)
  4. must set it explicitly — consumer side
  5. plain non-tx records always visible

basics

~10 s

read_committed makes the consumer skip records from aborted transactions and not read records from transactions that are still open. It only sees data the producer actually committed, so a rolled-back send never becomes visible.

solid answer

~40 s

A Kafka consumer's isolation.level is either read_uncommitted (the default) or read_committed. With read_uncommitted the consumer sees every record, including ones written by transactions that later abort. With read_committed the consumer only delivers records from committed transactions: it filters out aborted records and, crucially, will not read past the Last Stable Offset (LSO) — so records belonging to a transaction that is still in flight stay invisible until that transaction commits or aborts. This is what makes producer-side transactions actually useful end to end: if the produce side rolls back, downstream read_committed consumers never observe the phantom messages. In Spring you set it via consumer property spring.kafka.consumer.isolation-level=read_committed (or ConsumerConfig.ISOLATION_LEVEL_CONFIG). Without it, transactional producers give you atomic writes but consumers can still read data that gets aborted.

code

properties · 4 lines
properties
# application.properties — make listeners honor producer transactions
spring.kafka.consumer.isolation-level=read_committed
spring.kafka.consumer.enable-auto-commit=false
spring.kafka.consumer.group-id=orders-processor

go deeper

for a junior

Know the two values, that read_committed hides aborted records, and that it is not the default.

for a middle

Explain the LSO and the latency trade-off, and where to set it in Spring.

for a senior

Tie it to end-to-end EOS: consumer read_committed plus a transactional producer are both required.

for a principal

Discuss operational impact — long-open transactions stalling read_committed consumers and monitoring LSO lag.

## The problem Kafka transactions let a producer write to multiple partitions (and commit consumer offsets) atomically — all messages become visible together or none do. But a transaction only helps if *readers* respect it. `isolation.level` is the **consumer** setting that decides whether readers honor transaction boundaries. ## The two values - **`read_uncommitted`** (the default): the consumer receives *all* records as soon as they are appended to the log, including records written by a transaction that is later **aborted** (rolled back). You get 'dirty reads'. - **`read_committed`**: the consumer only delivers records from **committed** transactions. It does two things: 1. **Filters aborted records** — the broker knows (via control/marker records) which records belonged to an aborted transaction and does not hand them to the consumer. 2. **Respects the Last Stable Offset (LSO)** — the consumer will not read beyond the LSO, which is the offset of the first still-open transaction. So records from an *in-flight* transaction stay invisible until it commits or aborts. This can add latency: a slow or hung transaction blocks visibility of everything after it. Non-transactional (plain) records are always visible under both settings — `read_committed` only gates *transactional* records. ## How to set it in Spring ```properties spring.kafka.consumer.isolation-level=read_committed ``` or programmatically via `ConsumerConfig.ISOLATION_LEVEL_CONFIG = "read_committed"`. In a `@KafkaListener` / `ConcurrentKafkaListenerContainerFactory` setup this flows through the consumer factory. ## Why it matters for exactly-once Exactly-once semantics (EOS) for a read-process-write pipeline depends on **both** ends: the producer wraps produce + offset-commit in a transaction, *and* downstream consumers run `read_committed` so aborted work is never observed. If you forget the consumer setting, a rolled-back batch can still be read and processed — breaking the guarantee. ## Gotchas - `read_committed` is **not the default** — you must set it explicitly. - It adds visibility latency proportional to how long transactions stay open; keep transactions short. - It does not deduplicate or make *your* consumer transactional; it only hides aborted/in-flight transactional records. - Control records (transaction markers) are consumed internally and never delivered to your application.

  • What is the Last Stable Offset and how can read_committed add latency?
    The LSO is the offset of the first still-open transaction; a read_committed consumer won't read past it. If a producer holds a transaction open (or hangs before commit/abort), consumers stall on everything after the LSO until it resolves, so long-running transactions add visibility latency.
  • Does read_committed guarantee exactly-once on its own?
    No. It only ensures consumers don't see aborted/in-flight transactional records. Exactly-once also needs the producer side to be transactional (transactional.id + atomic produce plus offset commit). Both ends together give the guarantee.

saying these in an interview costs you the question

  • Thinking read_committed is the default (it is not — read_uncommitted is).
  • Believing read_committed deduplicates or makes the consumer itself transactional.
  • Assuming read_committed hides plain non-transactional messages too.
  • Not realizing it can add latency by blocking at the LSO.

context