Skip to main content
José David Baena

On this page

A Kafka DLQ Write and Offset Commit Are Two Operations—Unless You Join Them

By José David Baena

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.

OrderCrash pointNormal group recovery
Commit source 11, then publish DLQCrash after commitStarts at 11; no durable quarantine exists
Publish DLQ, then commit source 11Crash after publishStarts at 10; quarantine publication can repeat
One Kafka transactionCommit or abort the DLQ record and source 11 togetherBoth 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?

Choose a Kafka teaching scenario
Change bounded local inputs

Changing these controls creates a custom local example. The share link keeps only the original curated preset; exports can include your bounded local values.

The planner never executes the selected operation.

Only one same-Kafka transaction can join those Kafka participants.

Retry topics do not preserve partition processing order by default.

Independent from group checkpoints and sink protection.

A source record can outlive this boundary.

At/after expiry is tested explicitly.

Includes the first attempt.

Conditional nested upper bound only.

Do not assume all layers multiply independently in production.

0–600,000 virtual milliseconds.

Current outcome · step 0 of 5

The failed source record remains at offset 10; no quarantine publication, progress change, or recovery authority exists yet.

curated preset

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.

Ordered deterministic actions through step 0. Print and exports contain the complete bounded trace.
StepActorActionObserver result
10 msoperatorBegin ordinary Kafka transactionAn ordinary same-cluster Kafka transaction begins. No source progress or quarantine record is committed yet.
21 msoperatorAppend synthetic DLQ recordThe synthetic quarantine record is acknowledged under the selected Kafka publication contract.
32 msoperatorAttach source next offset 11Source next offset 11 is attached to the pending Kafka transaction.
43 msprocessCrash before commit requestThe crash occurs before any commit request. The bounded ordinary transaction resolves as abort; source progress remains unchanged.
54 msoperatorRestore input positionInput position is restored to stored next offset 10.

Evidence and limits

Claims

Primary sources

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.
Default synthetic example loaded.

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:

  1. offset 10 fails and moves to a 5-minute retry topic;
  2. source progress advances to 11;
  3. offset 11 applies immediately;
  4. offset 10 returns later.

The original partition order has reversed at the business effect. Choose one policy:

PolicyBenefitCost
Stop the partition/keyPreserves local orderOne failure blocks later work
Retry topic + per-key sequencingAllows unrelated keys to progressNeeds durable sequence/version checks and another state store
Retry topic + relaxed orderSimple throughput pathBusiness 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:

  1. topic history and compaction;
  2. coordinator-stored group checkpoints and reset policy;
  3. source outbox/CDC history;
  4. 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

HNPost to Hacker News

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.

Keep reading