Monedula

Kafka troubleshooting

The consumer reprocesses messages after a restart

Kafka symptom

A restarted consumer resumes from its group's committed offset, not from the last record it processed, so everything processed after the last successful commit is delivered again. A few records is normal at-least-once behaviour. A full replay means the committed offset was missing and auto.offset.reset=earliest sent the group back to the start of the log.

Reproduce it in the playground

The moment it happensRe-reading O1-O3 again. t+3.200s

One record per poll (max.poll.records=1), g1 works back through the same offsets it consumed before - position is 2, so O1 and O2 have been delivered a second time. The log was never consumed-away; the offset is just a movable bookmark.

Open at this step: step 4, Re-reading O1-O3 again

What it means

The consumer tracks two positions per partition: its position, the next offset it will read, and the committed offset, stored by the group coordinator, which is where it recovers to after a restart or when another member takes the partition over. With enable.auto.commit=true, the default, the client commits its position in the background every auto.commit.interval.ms (5 seconds by default) and once more in close(). With manual commits, the committed offset moves only when your code calls commitSync() or commitAsync(). Either way, the gap between the two positions at the moment the process stops is what gets processed again. On startup the consumer logs where it resumes: "Setting offset for partition orders-0 to the committed offset FetchPosition{offset=1500, ...}" when the group has a committed offset, or "Found no committed offset for partition orders-0" and then "Resetting offset for partition orders-0 to position FetchPosition{offset=0, ...}." when auto.offset.reset decides. The replay scenario reproduces the full-replay case: the group comes back with no committed offset and auto.offset.reset=earliest re-reads the log.

Likely causes, most common first

  1. The process was killed instead of closed. close() commits the current offsets when auto commit is on, and a dynamic member also leaves the group. A process stopped with SIGKILL, killed for running out of memory, or given less time to stop than close() needs (up to 30 seconds by default) skips that commit, so up to one auto-commit interval of work is replayed.
  2. Commits are coarse or failing. Code that commits once per batch replays the whole batch after a crash. Failed commitAsync() calls are easy to miss, and a commit made after the member was evicted for exceeding max.poll.interval.ms fails with CommitFailedException, so the new owner processes that batch again.
  3. The committed offset is gone. Offsets of a group with no members expire after offsets.retention.minutes (7 days by default). A group stopped for longer than that, a changed group.id, or a committed offset that retention has since deleted all leave the consumer without a valid offset, and auto.offset.reset decides where it starts: earliest replays the whole retained log, latest (the default) skips to the end.
  4. A rebalance handed over uncommitted work. While an instance restarts, its partitions move to other members. If it did not commit in onPartitionsRevoked, the new owner starts from the older committed offset. When a session times out, the partitions can be reassigned before revocation runs at all.
  5. The duplicates are in the log. If the same payload sits at two different offsets, the consumer is not reprocessing anything: a producer wrote the record twice, typically after a retry with idempotence off or a resend from application code.

How to fix it

What not to do

Configs involved

Reproduce it in the simulator

Sources

Related

All troubleshooting pages