A Kafka DLQ Write and Offset Commit Are Two Operations—Unless You Join Them
- Published on
- /8 mins read

Offset 10 fails decoding. The handler writes a dead-letter record and commits 11. A crash between those operations decides whether the failed work disappears from normal recovery or appears in quarantine twice.
A dead-letter queue (DLQ) is only durable disposition when the quarantine record, source progress, provenance, and later redrive contract all hold. Plain KafkaConsumer does not create that contract automatically.
This guide uses a stricter recovery question: what evidence and consistency boundary must exist before we retry, quarantine, or replay? Its strongest result is “candidate for human review,” never “safe to run.”
Commit-first loses disposition; publish-first can duplicate it
Start with one failed source record at offset 10. The next source offset is 11.
| Order | Crash point | Normal group recovery |
|---|---|---|
| Commit source 11, then publish DLQ | Crash after commit | Starts at 11; no durable quarantine exists |
| Publish DLQ, then commit source 11 | Crash after publish | Starts at 10; quarantine publication can repeat |
| One Kafka transaction | Commit or abort the DLQ record and source 11 together | Both Kafka participants share one decision under the transaction prerequisites |
The first branch skips work without disposition. The second duplicates quarantine, which is not automatically a duplicate business effect. Preserve the original cluster/topic/partition/offset, event ID, operation ID, schema, failure stage, and attempt history so later review can identify repeated quarantine.
The Kafka producer transaction API can include output records and consumed next offsets in one same-cluster transaction. The positive DLQ path needs:
- an ordinary transactional producer with
transaction.two.phase.commit.enable=false; - current consumer group metadata;
- the source next offset, not the failed offset;
- a DLQ encoding that succeeded;
- durable topic/internal-topic settings;
- committed-only readers when upstream transactions matter;
- abort recovery that restores the input position.
It does not make the later redrive effect atomic.
Deterministic teaching model · apache-java-4.3.1
R03: One Kafka transaction can join DLQ and source progress
Which participants share the decision?
Current outcome · step 0 of 5
The failed source record remains at offset 10; no quarantine publication, progress change, or recovery authority exists yet.
Default synthetic example loaded.
Disposition consistency
- Source stored next offset
- 10
- Quarantine publications / visible
- 0 / no
- Transaction decision / response
- none / none
- Input restored
- no
Retry and ordering
- Conditional attempt upper bound
- 8
- Attempts inside deadline
- 0
- 0 ms of 30000 ms.
- Ordering contract
- partition-order
- Later record completed first
- no
Evidence and authority
- effect
- unknown
- identity
- unknown
- authority
- unknown
- dependency
- unknown
- schema
- unknown
- sourceRange
- unknown
- Protection current
- yes
- Suggested disposition
- needs reconciliation
Runbook
- Canary
- not recorded
- Stop / verification items
- 0 / 0
- Manifest
- not requested
- Cross-cluster profile
- same-cluster
Invariant results
Every normally skipped failed record has a durable disposition
holds in model
Source progress has not crossed the failed record without visible quarantine.
Transactional DLQ and source progress share one decision
holds in model
A committed recovery transaction exposes both Kafka participants together in the bounded model.
Retry flow preserves the declared ordering contract
holds in model
No selected action contradicts the declared order.
Reported evidence establishes automatic replay permission
violated in model
Missing, negative, or expired evidence blocks automatic effect-capable replay.
Ordered trace
The state table shows model truth. Each observer result says what the current caller can establish at that step.
| Step | Actor | Action | Observer result |
|---|---|---|---|
| 10 ms | operator | Begin ordinary Kafka transaction | An ordinary same-cluster Kafka transaction begins. No source progress or quarantine record is committed yet. |
| 21 ms | operator | Append synthetic DLQ record | The synthetic quarantine record is acknowledged under the selected Kafka publication contract. |
| 32 ms | operator | Attach source next offset 11 | Source next offset 11 is attached to the pending Kafka transaction. |
| 43 ms | process | Crash before commit request | The crash occurs before any commit request. The bounded ordinary transaction resolves as abort; source progress remains unchanged. |
| 54 ms | operator | Restore input position | Input position is restored to stored next offset 10. |
Evidence and limits
Assumptions and known limits
- All ranges, identities, and evidence answers are synthetic and bounded.
- The planner never contacts Kafka or emits executable reset, delete, or redrive commands.
- Reported evidence is not verified evidence or replay authorization.
Compare R01, R02, R03, and R04. The state changes through reducer actions: source progress, quarantine visibility, transaction decision, caller knowledge, and restored input position remain independent.
Crash before commit and lost commit response are not the same history
If the process crashes before issuing a commit request, the bounded ordinary transaction can later resolve as abort. The DLQ record remains hidden, source progress remains 10, and recovery returns to the failed input.
If the coordinator accepts commit and the response disappears, the caller is UNKNOWN. The transaction may already have committed both participants. commitTransaction()'s method contract requires retrying the same commit operation or closing and recovering. Switching to abort because the response was late can contradict the terminal decision.
The same rule applies to an uncertain abort: repeat abort or close. Do not build one catch (KafkaException) { abort(); retryEverything(); } policy across serialization, authorization, fencing, timeout, and stale group metadata.
Retry topics trade immediate progress for ordering
A retry topic removes the failed record from the original processing path and allows later original-partition records to advance. That can be correct, but it changes the order contract.
Suppose offsets 10 and 11 share one account key:
- offset 10 fails and moves to a 5-minute retry topic;
- source progress advances to 11;
- offset 11 applies immediately;
- offset 10 returns later.
The original partition order has reversed at the business effect. Choose one policy:
| Policy | Benefit | Cost |
|---|---|---|
| Stop the partition/key | Preserves local order | One failure blocks later work |
| Retry topic + per-key sequencing | Allows unrelated keys to progress | Needs durable sequence/version checks and another state store |
| Retry topic + relaxed order | Simple throughput path | Business logic must tolerate reordering |
Kafka does not imply a per-record delay scheduler, priority queue, or workflow state machine because a team created orders-retry-5m.
Retry budgets name owners and one absolute deadline
Broker/client retry, producer retry, application resend, handler retry, framework redelivery, and operator redrive are not automatically independent. Write one owner per boundary.
If three layers each allow two total attempts, the mechanical upper bound is 2 × 2 × 2 = 8. That bound applies only when every layer fully nests inside the next one. An absolute deadline can clip it; backpressure can prevent later attempts from starting; a non-retriable error can stop the chain.
More attempts do not resolve an unknown effect. They only create more chances to repeat it. Use the existing Retry Budget & Amplification Workbench for the bounded timeline, then return here for Kafka-specific identity, order, progress, and visibility.
Replay readiness depends on four retention systems
Retained source records do not establish a complete replay. Review:
- topic history and compaction;
- coordinator-stored group checkpoints and reset policy;
- source outbox/CDC history;
- sink identity/result retention.
The topic configuration and consumer reset configuration answer different parts. A checkpoint can expire while data remains. A compacted topic can retain current keys while omitting historical operations. An open transaction can hold read_committed visibility below the high watermark.
At the equal boundary, define the rule. If protection is valid through day 30, does replay at the start of day 30 remain covered, or does expiry happen first? Tests need one deterministic ordering. Production stores need a documented clock, time zone, restore behavior, and key-reuse policy.
Original intent and a new operation need different authority
Redriving the original business action should preserve its operation identity. Changing the key to bypass a duplicate check creates a new operation from the sink's perspective.
Sometimes a new operation is correct: an operator may approve a replacement payment, email, or credit after reconciliation. Record that as a separate authorized action with a new operation ID and a link to the original. Do not smuggle it through as “the same retry with a fresh key.”
If effect truth or current authority is unknown, stop automatic effect-capable replay. A compensation is also a new authorized effect, not a rollback that Kafka performs.
A replay manifest records questions, not commands
The planner exports a synthetic manifest shaped like this:
{
"formatVersion": 1,
"synthetic": true,
"baselineId": "apache-java-4.3.1",
"sourceIncarnation": "teaching-cluster-a/topic-a-v1",
"ranges": [
{
"partition": 0,
"startInclusive": 10,
"endExclusive": 11,
"isolation": "read_committed",
"completeness": "unknown"
}
],
"intentPolicy": "preserve-original",
"stopConditionIds": ["stop-on-identity-conflict"],
"verificationIds": ["verify-business-effect-count"]
}The range is half-open: include 10, stop before 11. Real Kafka offsets can exceed JavaScript's safe integer range, so broker-derived evidence must keep them as decimal strings. The browser model stays inside small teaching integers.
The export contains no kafka-consumer-groups --reset-offsets, topic delete, produce, SQL mutation, or provider call. It records:
- source and target reader versions;
- schema interpretation;
- original coordinates and identity;
- retention evidence;
- dry-run limits;
- owner and approver roles;
- canary range and rate budget;
- stop conditions;
- business-state verification after progress advances.
Use the general Replay Readiness Checklist for the cross-system review and Dead-Letter Queue Triage for the next evidence-gathering step.
Cross-cluster recovery needs another authority model
MirrorMaker can replicate records and, in a documented dedicated setup, use an EOS mode for the replication leg. The Kafka 4.3 geo-replication guide does not turn a source application transaction into one cross-cluster commit.
Failover must reconcile:
- which records arrived;
- which checkpoints translated;
- which schemas and keys arrived;
- which sink remembers operation identities;
- which cluster may accept authoritative writes;
- how failback avoids dual authority.
R12 therefore returns OUTSIDE_MODEL. It links the portability boundary audit instead of exporting a same-cluster “safe failover” plan.
The recorded F09 fixture narrows one example: pinned Kafka 4.3.1 MirrorMaker copied three synthetic records, synchronized an inactive group at next offset two, and let a target reader resume after the owned source brokers stopped. That profile enabled no target writer and tested no failback. It is broker evidence for one manual read cutover, not permission to reuse the schedule for another topology.
What could go wrong
- The DLQ payload cannot encode the poison record. Committing progress anyway recreates the commit-first loss window.
- The DLQ reader uses
read_uncommitted. It can act on a quarantine record from an aborted disposition transaction. - A retry topic keeps source progress moving but no key-order policy exists. Later changes overtake earlier ones.
- Topic retention exists but the sink's operation keys expired. Replay can repeat effects.
- A dry run reads bytes successfully and is called proof. It did not verify current authority, provider state, capacity, or complete history.
- A copied reset command targets the wrong group or cluster. This guide never emits live commands or accepts a bootstrap server.
Recovery begins when the system admits what it does not know. Keep that uncertainty visible until evidence, identity, and authority resolve it.
Sources and references
Share this post
Follow future work
Follow public article updates through RSS. Intentionally unlisted posts stay out of the feed.
Working through a similar reliability boundary?
The Async Reliability Review turns one messaging or background-job flow into an evidence map, recovery plan, and owned next actions.

