Kafka troubleshooting
The consumer group keeps rebalancing
A consumer group rebalances whenever a member joins, leaves or is declared dead, so repeated rebalances mean members keep dropping out and coming back. The usual causes are processing that takes longer than max.poll.interval.ms, heartbeats missing for longer than the session timeout, and restarts of members without a static group.instance.id. A member removed for a slow poll loop logs "consumer poll timeout has expired", and its next commitSync() fails with CommitFailedException. Find which one applies before raising any timeout.
Reproduce it in the playground3 steps beforeTwo-member group ready. t+0.000s
A 2-partition topic with a cooperative-sticky group of two consumers: c1 owns p0, c2 owns p1. max.poll.interval.ms is 2000 ms. Press play.
Open at this step: step 1, Two-member group ready2 steps beforeBoth consuming · lag 0. t+0.900s
One record landed on each partition; c1 drained p0 and c2 drained p1. Both members are caught up.
Open at this step: step 2, Both consuming · lag 01 step beforec1 stuck processing · p0 stalls, c2 keeps consuming. t+2.500s
c1 got stuck processing at t=1000. It still heartbeats (its session never times out) and still owns p0, but it no longer polls - so new records on p0 are NOT consumed and its lag grows, while c2 keeps draining p1. Unlike a crash, the member is alive: this is the max.poll.interval.ms failure, not session.timeout.ms.
Open at this step: step 3, c1 stuck processing · p0 stalls, c2 keeps consumingThe moment it happensEvicted · survivor took over. t+4.000s
At t=3000 max.poll.interval.ms expired: c1's heartbeat thread left the group (max.poll.interval.ms exceeded) and p0 rebalanced onto the survivor c2, which now owns both partitions and has drained p0 back to lag 0 - the same revoke→assign rebalance a crash eviction uses.
Open at this step: step 4, Evicted · survivor took overWhat it means
Each partition in a group is owned by exactly one member at a time, and the group coordinator reassigns partitions when membership changes or when a subscribed topic gains partitions. A member is removed in two ways: it stops heartbeating for longer than the session timeout, or it does not call poll() within max.poll.interval.ms, in which case the client leaves the group on its own and logs "consumer poll timeout has expired. This means the time between subsequent calls to poll() was longer than the configured max.poll.interval.ms, which typically implies that the poll loop is spending too much time processing messages." (the KIP-848 consumer writes "Consumer poll timeout has expired."). On the classic protocol, a commitSync() from a member that was removed fails with CommitFailedException: "Commit cannot be completed since the group has already rebalanced and assigned the partitions to another member." The KIP-848 consumer reports the same situation as "OffsetCommit failed with stale member epoch." or "OffsetCommit failed with unknown member ID." On the classic protocol, still the client default as of Kafka 4.3, every removal and every rejoin is a rebalance, and with the default RangeAssignor every member revokes all of its partitions first, so the whole group stops consuming while it runs. The consumer group protocol from KIP-848 (group.protocol=consumer, generally available since Kafka 4.0) is incremental and moves only the partitions that change owner, but a member that keeps timing out still causes churn.
Likely causes, most common first
- Processing takes longer than max.poll.interval.ms. If more than max.poll.interval.ms (5 minutes by default) passes between two poll() calls, the consumer leaves the group and its partitions move. Its next poll() rejoins, the same slow batch arrives again, and the cycle repeats. A commitSync() after the eviction fails with CommitFailedException, so that batch is processed a second time. A large max.poll.records (500 by default) combined with slow per-record work is the usual trigger. A static member (group.instance.id set) is the exception: it stops heartbeating but keeps its partitions until the session timeout expires, and only then are they reassigned.
- Heartbeats stop for longer than the session timeout. Long garbage collection pauses, CPU throttling in a container, or network trouble between the client and the group coordinator delay heartbeats. On the classic protocol the limit is session.timeout.ms (45 seconds by default, with heartbeat.interval.ms at 3 seconds). With group.protocol=consumer both are set on the broker, by group.consumer.session.timeout.ms and group.consumer.heartbeat.interval.ms.
- Deploys and restarts of dynamic members. Without group.instance.id, a consumer that shuts down leaves the group, and when it starts again it joins as a new member, so a rolling deploy triggers at least one rebalance per instance (KIP-345). Autoscalers that keep adding and removing consumers have the same effect.
- Every rebalance stops the whole group. With the eager RangeAssignor a rebalance revokes every partition from every member. That does not cause more rebalances on its own, but it turns each one into a full stop of consumption, which is usually what makes the problem visible as lag.
How to fix it
- Confirm the pattern first. kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group my-group --members lists the current member ids, and --state shows whether the group is Stable. On the client, rebalance-rate-per-hour and last-rebalance-seconds-ago show how often it happens, and time-between-poll-max shows how close the gaps between polls get to max.poll.interval.ms.
- If poll gaps are the cause, lower max.poll.records so a batch fits the interval, make processing faster, or raise max.poll.interval.ms to cover the real worst case. When processing time is unpredictable, the KafkaConsumer Javadoc recommends handing records to another thread and pausing the partitions so poll() keeps being called.
- If heartbeats are the cause, fix the pause itself (GC settings, CPU limits) before touching timeouts, and keep heartbeat.interval.ms at no more than a third of session.timeout.ms.
- For deploys, give every instance a stable, unique group.instance.id. A static member does not send a leave request when it shuts down, and if it returns within the session timeout it gets its old partitions back with no rebalance.
- Move off the eager assignor. CooperativeStickyAssignor is already the second entry in the default partition.assignment.strategy, so one rolling bounce that removes RangeAssignor from the list switches the group to cooperative rebalancing. Alternatively, move the group to group.protocol=consumer, where the broker computes assignments.
What not to do
- Setting session.timeout.ms or max.poll.interval.ms very high to stop the rebalances. A consumer that has really died then keeps its partitions for that long, and a large max.poll.interval.ms delays every rebalance, because a member only joins one inside poll().
Configs involved
max.The longest a group member may go between poll() calls before it counts as failed and the group rebalances its partitions to other members (default 300000, 5 minutes); a static member keeps them until its session times out.poll. interval. ms max.The most records one poll() returns (default 500); a smaller value shortens the processing between two polls, which keeps a group member inside max.poll.interval.ms.poll. records session.How long the group coordinator waits for a heartbeat from a classic-protocol consumer before it removes the member and rebalances (default 45000, 45 seconds); with group.protocol=consumer the broker group.consumer.session.timeout.ms applies instead.timeout. ms heartbeat.How often a classic-protocol consumer sends a heartbeat to the group coordinator (default 3000, 3 seconds); keep it at no more than a third of session.timeout.ms.interval. ms group.Makes the consumer a static member: after a restart within the session timeout it gets its partitions back without a rebalance, and only one member with this id can be in the group at a time.instance. id partition.The assignors a classic-protocol consumer supports, in order of preference; the default [RangeAssignor, CooperativeStickyAssignor] assigns by range, and CooperativeStickyAssignor moves only the partitions that change owner instead of revoking every partition on each rebalance.assignment. strategy group.The rebalance protocol of the consumer: classic (the default as of Kafka 4.3) or consumer, the KIP-848 protocol where the broker computes the assignment and the session timeout and heartbeat interval are broker settings.protocol
Reproduce it in the simulator
- Kafka max.poll.interval.ms: evicting a stuck consumer (Stuck consumer & max.poll.interval.ms) Open at the moment it happens: Kafka max.poll.interval.ms: evicting a stuck consumer
- Kafka consumer crash and session.timeout.ms (Consumer crash & session timeout) Open at the moment it happens: Kafka consumer crash and session.timeout.ms
- Kafka static membership: group.instance.id avoids rebalances (Static membership: avoid rebalance) Open at the moment it happens: Kafka static membership: group.instance.id avoids rebalances
- Kafka eager vs cooperative rebalancing (cooperative-sticky) (Rebalance: eager vs cooperative) Open at the moment it happens: Kafka eager vs cooperative rebalancing (cooperative-sticky)
Sources
- Apache Kafka 4.3: consumer config max.poll.interval.ms
- Apache Kafka 4.3: consumer config session.timeout.ms
- Apache Kafka 4.3: consumer config group.instance.id
- Apache Kafka 4.3: consumer config partition.assignment.strategy
- Apache Kafka 4.3: consumer rebalance protocol (KIP-848)
- Apache Kafka 4.3 Javadoc: KafkaConsumer, detecting consumer failures
- Apache Kafka 4.3: consumer group metrics
- KIP-345: static membership
- KIP-429: incremental cooperative rebalancing
- Apache Kafka 4.3.0 source: AbstractCoordinator.java (consumer poll timeout has expired)
- Apache Kafka 4.3.0 source: AbstractHeartbeatRequestManager.java (KIP-848 consumer poll timeout)
- Apache Kafka 4.3.0 source: CommitFailedException.java (default message)
- Apache Kafka 4.3.0 source: CommitRequestManager.java (KIP-848 commit failures)