Symptoms
- Logs full of
Revoking previously assigned partitions,(Re-)joining group,Successfully joined group with generation Nwhere N climbs quickly. - Consumer lag grows in waves; throughput drops to zero during each rebalance.
- Messages processed twice after rebalances (offsets not committed before revocation).
CommitFailedException: ... the group has already rebalanced and assigned the partitions to another member.
Why consumers leave the group
A consumer is removed and a rebalance starts when:
| Trigger | Setting | Default (Java client) |
|---|---|---|
No poll() call for too long (processing too slow) | max.poll.interval.ms | 300000 (5 min) |
| No heartbeats (process frozen, GC pause, network) | session.timeout.ms | 45000 (Kafka 3.0+) |
| Consumer starts, stops or restarts | — | — |
| Topic partition count changes / subscription pattern matches new topics | — | — |
Find which one from the client logs. The Java client logs, for example:
consumer poll timeout has expired. This means the time between subsequent calls to poll()
was longer than the configured max.poll.interval.ms ... sending LeaveGroup request
Step 1: look at the group
kafka-consumer-groups.sh --bootstrap-server broker1:9092 --command-config client.properties \
--describe --group orders-svc --state
kafka-consumer-groups.sh --bootstrap-server broker1:9092 --command-config client.properties \
--describe --group orders-svc --members --verbose
State alternating between PreparingRebalance/CompletingRebalance and Stable, and member IDs that keep changing, confirm the storm.
Step 2: slow processing (max.poll.interval.ms)
If a batch from poll() takes longer than max.poll.interval.ms to process, the consumer is kicked out.
| Fix | How |
|---|---|
| Fetch fewer records per poll | Lower max.poll.records (default 500) |
| Allow longer processing | Raise max.poll.interval.ms (also delays detection of truly stuck consumers) |
| Make processing faster | Batch downstream writes, async I/O, avoid per-message remote calls |
| Hand work to a thread pool | Pause partitions (consumer.pause()) while workers process, keep calling poll(), resume after; commit only completed offsets |
max.poll.records=100
max.poll.interval.ms=600000
Step 3: heartbeats (session.timeout.ms)
Heartbeats run on a background thread, so slow processing doesn't stop them, but long GC pauses, CPU throttling in containers, or network issues do.
- Check GC logs and container CPU throttling (
container_cpu_cfs_throttled_seconds_total). - Keep
heartbeat.interval.msat about a third ofsession.timeout.ms. - Raise
session.timeout.msmoderately if pauses are short and unavoidable (the broker limits the range withgroup.min/max.session.timeout.ms).
Step 4: stop "stop-the-world" rebalances
With the classic eager protocol, every member gives up all partitions on each rebalance. Use cooperative rebalancing so only moved partitions are revoked:
partition.assignment.strategy=org.apache.kafka.clients.consumer.CooperativeStickyAssignor
Switching a live group from eager to cooperative needs a two-step rolling deployment (first list both assignors, then remove the eager one). See the client documentation for the safe upgrade path.
Step 5: rolling restarts and Kubernetes
Every pod restart triggers two rebalances (leave and join). A rolling deploy of 20 pods = 40 rebalances.
Static membership gives each consumer a stable identity, so a restart within session.timeout.ms doesn't trigger a rebalance:
group.instance.id=${POD_NAME} # must be unique and stable per instance (StatefulSet names work well)
session.timeout.ms=120000 # long enough to cover a restart
Trade-off: if a member really dies, its partitions aren't reassigned until the session timeout expires.
Step 6: the new consumer group protocol (KIP-848)
Kafka 4.0 made the new consumer rebalance protocol generally available. The broker computes assignments and members reconcile incrementally, without a group-wide synchronisation barrier, which removes most stop-the-world behaviour.
group.protocol=consumer
It needs brokers and clients that support it (Kafka 4.0+ brokers in KRaft mode, recent Java client; other clients are catching up). Some classic settings, such as client-side partition.assignment.strategy, don't apply in this mode; assignment is configured on the broker side. Test in non-production first.
Step 7: avoid duplicate processing during rebalances
- Commit offsets for processed records in
onPartitionsRevoked(classic protocol) before partitions move. - Make processing idempotent (upserts, dedup keys), because at-least-once delivery means some duplicates are always possible.
consumer.subscribe(List.of("orders"), new ConsumerRebalanceListener() {
public void onPartitionsRevoked(Collection<TopicPartition> parts) { consumer.commitSync(currentOffsets(parts)); }
public void onPartitionsAssigned(Collection<TopicPartition> parts) { }
});
Decision table
| Log/metric evidence | Fix |
|---|---|
| "poll timeout has expired" | Fewer records per poll, faster processing, or longer max.poll.interval.ms |
| Session timeouts with long GC / CPU throttling | Fix memory/CPU limits; moderate session.timeout.ms increase |
| Rebalance per pod restart | Static membership (group.instance.id) |
| All consumers pause on each rebalance | Cooperative sticky assignor, or the KIP-848 protocol |
| Rebalances when topics are created | Use explicit topic lists instead of broad regex subscriptions |
Prevent it next time
- Alert on consumer group rebalance rate (client metric
rebalance-rate-per-hourin the Java client) and on lag. - Use StatefulSets with static membership for long-running consumers on Kubernetes.
- Load test processing time per batch at peak, and set
max.poll.recordsfrom that. - Plan the move to the new consumer protocol once your clients support it.