Skip to content

AxonOps — AI-Native Control Plane for Open Source Data Platforms

Kafka Consumer Rebalancing

Rebalancing redistributes topic partitions among consumers when group membership or subscriptions change. Understanding rebalance behavior is critical for building resilient consumers.


TriggerDescription
Consumer joinsNew consumer added to group
Consumer leavesConsumer calls close()
Consumer crashesConsumer stops heartbeating
Session timeoutNo heartbeat within session.timeout.ms
Poll timeoutNo poll() within max.poll.interval.ms
TriggerDescription
Topic subscription changeConsumer changes subscribed topics
New partitions addedTopic partition count increases
Topic deletionSubscribed topic is deleted
TriggerDescription
Coordinator failoverGroup coordinator broker changes
Coordinator restartCoordinator broker restarts

All consumers stop processing during rebalance:

Eager rebalance protocol with all partitions revokedConsumer 1Consumer 2CoordinatorConsumer 3Consumer 1(P0, P1)Consumer 1(P0, P1)Consumer 2(P2, P3)Consumer 2(P2, P3)CoordinatorCoordinatorConsumer 3(joining)Consumer 3(joining)Processing activeJoinGroupRequestRebalanceRebalanceAll processing STOPSJoinGroup(revoke all)JoinGroup(revoke all)JoinGroupAssignment: P0Assignment: P2, P3Assignment: P1Processing resumes

Characteristics:

  • Stop-the-world: All partitions revoked
  • Maximum disruption during rebalance
  • Simple protocol logic

Only affected partitions pause:

Cooperative rebalance protocol revoking only reassigned partitionsConsumer 1Consumer 2CoordinatorConsumer 3Consumer 1(P0, P1)Consumer 1(P0, P1)Consumer 2(P2, P3)Consumer 2(P2, P3)CoordinatorCoordinatorConsumer 3(joining)Consumer 3(joining)Processing activeJoinGroupRequestRebalanceRebalanceContinue P0Revoke P1 onlyContinue P2, P3JoinGroup(P0, revoke P1)JoinGroup(P2, P3)JoinGroupAssignment: P0Assignment: P2, P3Assignment: P1Minimal disruption

Characteristics:

  • Incremental: Only moved partitions revoked
  • Minimal disruption to processing
  • Two-phase rebalance (may take longer)
partition.assignment.strategy=org.apache.kafka.clients.consumer.CooperativeStickyAssignor

Migration Required

Switching from eager to cooperative requires a rolling restart strategy. Consumers cannot mix protocols within a group.

  1. Configure both strategies (cooperative first):

    partition.assignment.strategy=org.apache.kafka.clients.consumer.CooperativeStickyAssignor,org.apache.kafka.clients.consumer.RangeAssignor
  2. Rolling restart all consumers

  3. Remove legacy strategy:

    partition.assignment.strategy=org.apache.kafka.clients.consumer.CooperativeStickyAssignor
  4. Rolling restart again


Join phase of a consumer group rebalanceConsumerCoordinatorConsumerConsumerCoordinatorCoordinatorJoinGroupRequest(group.id, member.id, subscription)Wait for all members(rebalance.timeout.ms)JoinGroupResponse(generation, leader, members)If leader: receives all member subscriptionsIf follower: receives empty member list
Sync phase distributing partition assignmentsLeaderCoordinatorFollowerLeaderLeaderCoordinatorCoordinatorFollowerFollowerCompute assignmentsSyncGroupRequest(assignments for all)SyncGroupRequest(empty)SyncGroupResponse(own assignment)SyncGroupResponse(own assignment)

Handle partition changes with ConsumerRebalanceListener:

public class MyRebalanceListener implements ConsumerRebalanceListener {
private final KafkaConsumer<String, String> consumer;
private final Map<TopicPartition, OffsetAndMetadata> pendingOffsets;
@Override
public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
// Called BEFORE partitions are revoked
log.info("Partitions revoked: {}", partitions);
// Commit pending work
if (!pendingOffsets.isEmpty()) {
consumer.commitSync(pendingOffsets);
pendingOffsets.clear();
}
// Flush any buffers
flushBuffers(partitions);
// Close partition-specific resources
for (TopicPartition partition : partitions) {
closePartitionState(partition);
}
}
@Override
public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
// Called AFTER partitions are assigned
log.info("Partitions assigned: {}", partitions);
// Initialize state for new partitions
for (TopicPartition partition : partitions) {
initializePartitionState(partition);
}
// Optionally seek to specific positions
// consumer.seek(partition, savedOffset);
}
@Override
public void onPartitionsLost(Collection<TopicPartition> partitions) {
// Cooperative only: partitions lost without clean revocation
log.warn("Partitions lost: {}", partitions);
// DO NOT commit - offsets may be stale
// Just clean up resources
for (TopicPartition partition : partitions) {
closePartitionState(partition);
}
}
}
consumer.subscribe(List.of("orders"), new MyRebalanceListener(consumer, pendingOffsets));

PropertyDefaultDescription
session.timeout.ms45000Time before consumer considered dead
heartbeat.interval.ms3000Heartbeat frequency
max.poll.interval.ms300000Maximum time between polls
rebalance.timeout.msmax.poll.interval.msTime allowed for rebalance

Consumers must complete rebalance within rebalance.timeout.ms:

rebalance.timeout.ms = max.poll.interval.ms (by default)

If a consumer doesn't respond within this time, it's removed from the group.


Reduce rebalances on planned restarts:

group.instance.id=consumer-instance-1
session.timeout.ms=300000

Retain partition assignments across rebalances:

partition.assignment.strategy=org.apache.kafka.clients.consumer.StickyAssignor

Only revoke partitions that must move:

partition.assignment.strategy=org.apache.kafka.clients.consumer.CooperativeStickyAssignor

Reduce processing time to avoid poll timeouts:

// Process asynchronously to avoid max.poll.interval.ms timeout
ExecutorService executor = Executors.newFixedThreadPool(10);
while (running) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
if (!records.isEmpty()) {
// Pause partitions during async processing
consumer.pause(consumer.assignment());
CompletableFuture<Void> processing = CompletableFuture.runAsync(
() -> processRecords(records),
executor
);
// Keep heartbeating while processing
while (!processing.isDone()) {
consumer.poll(Duration.ZERO); // Heartbeat only
Thread.sleep(100);
}
consumer.commitSync();
consumer.resume(consumer.assignment());
}
}

MetricDescription
rebalance-latency-avgAverage rebalance duration
rebalance-latency-maxMaximum rebalance duration
rebalance-totalTotal rebalances
rebalance-rate-per-hourRebalances per hour
failed-rebalance-totalFailed rebalances
last-rebalance-seconds-agoTime since last rebalance
// Access via JMX
ObjectName name = new ObjectName(
"kafka.consumer:type=consumer-coordinator-metrics,client-id=my-consumer"
);
Double rebalanceRate = (Double) mbs.getAttribute(name, "rebalance-rate-per-hour");
ConditionAction
Rebalance rate > 1/hourInvestigate cause
Failed rebalances > 0Check consumer health
Rebalance duration > 60sOptimize consumer

Symptoms:

  • High rebalance-rate-per-hour
  • Consumer lag increases during rebalances
  • Processing gaps in metrics

Common Causes:

CauseSolution
Processing too slowIncrease max.poll.interval.ms or process async
Network instabilityIncrease session.timeout.ms
GC pausesTune JVM, increase session.timeout.ms
Consumer crashesFix application bugs

Diagnostic Steps:

Terminal window
# Check consumer group state
kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
--describe --group order-processors --state
# Monitor rebalance events in logs
grep -i "rebalance\|revoked\|assigned" consumer.log

Symptoms:

  • Group in PreparingRebalance state
  • No progress in offset commits
  • Consumers not receiving messages

Solutions:

  1. Check for unresponsive consumers
  2. Verify network connectivity
  3. Force remove stuck consumer (where supported):
Terminal window
# Remove member from group (broker-side)
kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
--group order-processors --delete-members \
--member consumer-1-abc123

If --delete-members is unavailable, restart the stuck consumer to trigger a clean rebalance.

Symptoms:

  • Cascading rebalances
  • Multiple rebalances in quick succession

Prevention:

# Longer timeouts
session.timeout.ms=60000
max.poll.interval.ms=600000
# Static membership
group.instance.id=${POD_NAME}
# Cooperative rebalancing
partition.assignment.strategy=org.apache.kafka.clients.consumer.CooperativeStickyAssignor

PracticeRecommendation
Use cooperative rebalancingCooperativeStickyAssignor for Kafka 2.4+
Use static membershipStable deployments (Kubernetes)
Tune timeoutsBased on processing time and network
PracticeRecommendation
Handle rebalance eventsImplement ConsumerRebalanceListener
Commit before revokeEnsure progress is saved
Clean up resourcesClose partition-specific state
PracticeRecommendation
Monitor rebalance rateAlert on unexpected increases
Rolling deploymentsDeploy one consumer at a time
Test rebalance handlingVerify behavior under rebalance