skip to content

What is the WriteTxnMarkers request, and why must marker delivery be idempotent and epoch-fenced?

level: principalimportance: nice to knowfreq 18%

answer

  1. WriteTxnMarkers = inter-broker, API key 27
  2. carries PID, producerEpoch, coordinatorEpoch, result
  3. idempotent: failover re-drive must be a no-op
  4. epoch fences zombies (INVALID_PRODUCER_EPOCH)
  5. coordinatorEpoch fences deposed coordinator

basics

~20 s

WriteTxnMarkers is an inter-broker request the coordinator sends to each partition leader telling it to append a COMMIT/ABORT marker for a given PID and producer epoch. It must be idempotent (retries after a coordinator crash must not double-finalize) and epoch-fenced (a stale producer epoch is rejected to block zombies).

solid answer

~50 s

WriteTxnMarkers is the broker-to-broker API the transaction coordinator uses in phase 2 of commit. For each participating partition it carries the producerId, producerEpoch, transaction result (commit/abort), and the partition list. The receiving leader appends the corresponding control batch. Idempotency matters because coordinator failover re-drives markers from __transaction_state: a leader that already wrote the marker for that (PID, epoch) must treat a duplicate as a no-op, otherwise recovery would corrupt the log or mis-account the LSO. Epoch fencing matters because the epoch monotonically increases each time a transactional.id is re-initialized (initTransactions/InitProducerId); a leader rejects markers (and writes) carrying a producerEpoch older than the one it has seen, so a 'zombie' (an old producer instance that came back) cannot finalize or write into a transaction owned by a newer epoch. Together these make phase-2 propagation safe to retry indefinitely until every partition has its marker.

go deeper

for a junior

Know the coordinator tells partition leaders to write markers and that retries are safe.

for a middle

Identify WriteTxnMarkers as inter-broker and that it carries the PID, epoch, and commit/abort result.

for a senior

Explain idempotent re-drive on failover and producer-epoch zombie fencing.

for a principal

Tie idempotency + producer/coordinator epoch fencing to exactly-once atomic finalization under arbitrary crashes; reference KIP-98/KIP-447.

## What WriteTxnMarkers is `WriteTxnMarkers` (API key 27) is an **inter-broker** request — clients never send it. The **transaction coordinator** sends it to every broker that leads a partition involved in a transaction during **phase 2** of the commit/abort protocol. Each request entry contains: - `producerId` (PID) and `producerEpoch` — identifying the transactional producer instance. - `transactionResult` — COMMIT or ABORT. - `coordinatorEpoch` — the coordinator's own epoch, so a leader can reject markers from a stale coordinator. - the list of `TopicPartition`s to mark (including `__consumer_offsets` partitions when offsets were part of the transaction). The receiving leader appends a control batch (the marker) to each listed partition and replies. Only after **all** markers across **all** partitions are acknowledged does the coordinator append `CompleteCommit`/`CompleteAbort` to `__transaction_state`. ## Why idempotency is required Phase 2 is a *retryable propagation* of a decision that is already final (the PrepareCommit/PrepareAbort record). Failures force retries: - **Coordinator crash mid-phase-2**: the partition of `__transaction_state` fails over to a new broker. On load, the new coordinator replays the log, finds the transaction in PrepareCommit/PrepareAbort with no Complete record, and **re-sends WriteTxnMarkers** to all participants — including any that already received it. - **Partition-leader change**: the coordinator retries against the new leader. If marker appends were not idempotent, a re-sent marker could append a *second* COMMIT/ABORT control batch, double-advance accounting, or otherwise corrupt the log. So a leader recognizes that the transaction for that (PID, epoch) is already finalized in its log and treats the duplicate as a no-op. This lets the coordinator retry **indefinitely** with at-least-once delivery, achieving effectively-once finalization. ## Why epoch fencing is required The **producer epoch** is a monotonically increasing number assigned per `transactional.id`. Every `initTransactions()` (an `InitProducerId` call) bumps the epoch and aborts any in-flight transaction from the prior epoch. This is how Kafka fences **zombies** — an old producer process that hung (e.g., long GC) and then resumes, unaware a replacement took over its `transactional.id`. - A partition leader tracks the highest producerEpoch it has seen for a PID. - Any write — data or marker — carrying an **older** epoch is rejected with `INVALID_PRODUCER_EPOCH` / fencing errors. - So a zombie cannot append data, and a stale marker (from a delayed/old coordinator path) cannot finalize a transaction it no longer owns. The `coordinatorEpoch` field plays the analogous role for the coordinator: a leader rejects a `WriteTxnMarkers` from a coordinator whose epoch is older than what it has seen, preventing a deposed coordinator from interfering. ## Why both together give atomicity under failure - The **decision** is durable and single (phase-1 append). - **Idempotency** makes phase-2 safe to retry until every partition is marked. - **Epoch fencing** ensures only the legitimate current producer/coordinator can finalize, blocking zombies and split-brain. The net effect: the transaction commits or aborts atomically across all partitions exactly once, no matter how many brokers crash and how many retries occur. This is the foundation of Kafka's exactly-once semantics (EOS) for read-process-write pipelines (KIP-98, KIP-447 for the per-partition producer-id scaling).

  • Why does a re-sent WriteTxnMarkers after coordinator failover not corrupt the partition log?
    The leader recognizes the transaction for that (PID, epoch) is already finalized in its log and treats the duplicate marker as a no-op, so retries are idempotent.
  • How does the producer epoch fence a zombie producer?
    Each initTransactions() bumps the epoch; partition leaders reject any data or marker carrying an older producerEpoch (INVALID_PRODUCER_EPOCH), so a stale producer instance can neither write nor finalize a transaction.
  • What stops a deposed coordinator from writing markers?
    WriteTxnMarkers carries the coordinatorEpoch; leaders reject markers from a coordinator with a lower epoch than the one they've observed.

saying these in an interview costs you the question

  • Saying clients send WriteTxnMarkers (it is inter-broker only).
  • Thinking marker retries can double-commit (they are idempotent no-ops).
  • Believing the producer epoch is per-message rather than per transactional.id init.
  • Ignoring coordinatorEpoch fencing and only mentioning producerEpoch.

context