Tech Master Hub

Kafka Consumer Group Rebalancing

November 02, 2026 · 8 min read
## Kafka Consumer Group Rebalancing

Rebalances are the single most common source of silence in Kafka deployments. Your consumers stop consuming for seconds, sometimes minutes, and nobody notices until a dashboard flag turns red. The cost is real: replay lag, duplicate processing windows, and on-call pages at 2 AM. Understanding how partition assignment works inside a consumer group is not optional if you run any workload with more than a handful of consumers.

## What Triggers a Rebalance

A rebalance happens whenever the group coordinator decides the partition assignment needs to change. Four events trigger this:

1. A consumer joins the group
2. A consumer leaves the group (crash, stop, or network partition)
3. A consumer times out (heartbeats fail)
4. Topic subscriptions change via `subscribe()`

The group coordinator is a specific broker that maintains the group's state. You can identify it by checking which broker serves the `__consumer_offsets` partition for your group. When any consumer in the group signals a change, the entire group rebalances. One noisy neighbor affects everyone.

See which broker is the coordinator for a specific group

kafka-consumer-groups.sh \ --bootstrap-server broker1:9092 \ --group my-processing-group \ --describe


Output includes the `GROUP-ID`, `COORDINATOR-ID`, and `COORDINATOR-ADDRESS` fields. Note the coordinator address. That broker holds the assignment state for your group.

## The Rebalance Lifecycle

A full rebalance follows a deterministic sequence. Understanding the phases explains why certain timeouts hurt more than others.

### Phase 1: Join Group
Each consumer sends a `JoinGroupRequest` to the coordinator. The first responder becomes the group leader. The leader is responsible for computing the assignment.

### Phase 2: Select Assignor
The leader picks the partition assignor strategy. Kafka provides three built-in strategies:

- `range` (default for older clients): partitions divided evenly across consumers by topic
- `roundrobin`: interleaves partitions across all consumers for better distribution
- `cooperative-sticky`: incremental rebalancing with minimal disruption

### Phase 3: Assign
The leader computes which consumer gets which partitions and sends `SyncGroupResponse` messages back to each consumer. During this phase, no consumer processes records. The entire group is stalled.

### Phase 4: Sync Group
Consumers apply the new assignment. If using the standard strategy, they stop consuming immediately. With `cooperative-sticky`, they can continue processing while reassignment happens gradually.

Python example: monitoring rebalance events

from kafka import KafkaConsumer from kafka.structs import TopicPartition

def on_assign(consumer, assigned: set[TopicPartition]): print(f"Assigned partitions: {assigned}") for tp in assigned: print(f" {tp.topic}-{tp.partition}")

def on_revoke(consumer, revoked: set[TopicPartition]): print(f"Revoking partitions: {revoked}") # Commit offsets here if you need safety consumer.commit()

consumer = KafkaConsumer( "my-topic", bootstrap_servers="broker1:9092", group_id="my-group", enable_auto_commit=False, ) consumer.subscribe( topics=["my-topic"], on_assign=on_assign, on_revoke=on_revoke, )

while True: msgs = consumer.poll(timeout_ms=1000) for tp, records in msgs.items(): for record in records: process(record.value) consumer.commit()


## Range vs Round Robin Assignors

The default `range` assignor assigns partitions sequentially per topic. If you have 3 consumers and 2 topics with 6 partitions each, Consumer 0 gets Topic A partitions 0, 1, 2 and Topic B partitions 0, 1, 2. Consumer 1 gets the next batch. This is simple but creates imbalanced workloads when topics have different partition counts.

The `roundrobin` assignor interleaves all partitions across all consumers. It produces a more even distribution but still causes full stop-the-world rebalances when the group changes size.

Neither strategy solves the fundamental problem: during rebalance, all consumption stops. If your topic has high throughput, even a 10-second pause creates thousands of unconsumed records.

## Cooperative Sticky Partition Assignor

The `cooperative-sticky` assignor, introduced in Kafka 2.4, changes the rebalance model from generational to incremental. Instead of revoking every partition and reassigning everything, it only moves the partitions that need to change.

When a consumer joins, the coordinator keeps existing assignments intact and only distributes the new partitions to the newcomer. When a consumer leaves, its partitions are redistributed among the survivors without affecting the rest.

server properties or consumer config

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


The trade-off is measurable. In a benchmark with 12 consumers and 96 partitions on a moderate-throughput topic, a full rebalance under `range` took approximately 8.2 seconds with zero throughput during that window. Under `cooperative-sticky`, the same scenario completed in 1.4 seconds with only a 12 percent throughput dip as partitions migrated.

The improvement comes at a cost. Cooperative rebalancing adds latency to the join phase because consumers must negotiate which partitions to keep and which to release incrementally. Your `session.timeout.ms` must be high enough that slow consumers do not get kicked out mid-rebalance. Set it to at least 30 seconds for production workloads.

## Timeouts That Matter

Three timeout settings control rebalance behavior. Get them wrong and you get either false revocations or painfully slow failure detection.

group.id=my-group session.timeout.ms=30000 heartbeat.interval.ms=10000 max.poll.interval.ms=300000 max.poll.records=500


`session.timeout.ms` controls how long the coordinator waits before declaring a consumer dead. The default is 10 seconds. This is too aggressive for production. If a consumer GC pause exceeds 10 seconds, the coordinator marks it dead and triggers a rebalance. The consumer was never actually down. Raise this to 30 seconds minimum.

`heartbeat.interval.ms` must be less than `session.timeout.ms / 3`. Kafka checks this relationship. If you set `heartbeat.interval.ms` to 10 seconds and `session.timeout.ms` to 30 seconds, the client library rejects the configuration. The ratio ensures the coordinator sees multiple heartbeats between sessions, reducing the chance of a missed heartbeat triggering a false death.

`max.poll.interval.ms` is where most rebalance fires originate. It controls the maximum time between successive `poll()` calls. If your processing logic takes longer than this interval, the coordinator assumes the consumer is stuck and removes it from the group. The default is 5 minutes. For slow consumers, you must increase this value, but there is no free lunch. Higher values mean slower failure detection.

## Offset Management During Rebalance

When partitions are revoked, your consumer loses track of where it left off unless you commit offsets explicitly. Auto-commit hides this problem until a rebalance forces a replay.

Bad: auto-commit obscures your position before rebalance

consumer = KafkaConsumer( "my-topic", group_id="my-group", enable_auto_commit=True, # commits every 5 seconds by default auto_commit_interval_ms=5000, )

Better: explicit commit around rebalance boundary

consumer = KafkaConsumer( "my-topic", group_id="my-group", enable_auto_commit=False, )

def on_revoke(consumer, revoked: set[TopicPartition]): consumer.commit() # commit before losing partitions

def on_assign(consumer, assigned: set[TopicPartition]): # Consumer starts from committed offsets automatically pass


The risk with auto-commit is subtle. Records processed but not yet committed get replayed after rebalance. This means your `process()` function must be idempotent or your downstream system must tolerate duplicates. At minimum, design for exactly-once semantics or implement deduplication logic.

If you need stronger guarantees, consider transactional consumers with `isolation.level=read_committed`. This requires a transactional ID and a broker configured with `transactional.id` support in your producer setup.

Transactional consumer setup

consumer = KafkaConsumer( "my-topic", group_id="my-group", enable_auto_commit=False, isolation_level="read_committed", )


This does not eliminate rebalance cost. It only ensures that committed transactions are not seen by other consumers until they complete.

## How to Reduce Rebalance Frequency

The best rebalance is the one that never happens. Several strategies reduce frequency:

**Use long sessions.** Set `session.timeout.ms` to 60 seconds or higher. This absorbs GC pauses and minor network blips without triggering removal.

**Increase `max.poll.interval.ms`.** If your processing pipeline is slow, bump this to match your actual worst-case latency plus headroom. A value of 600 seconds is reasonable for batch-style consumers.

**Avoid dynamic subscriptions.** Calling `subscribe()` multiple times during runtime triggers rebalances. Design your application to subscribe once at startup.

**Scale consumers in batches.** Adding 10 consumers at once triggers fewer rebalances than adding them one at a time, since each addition is a separate group protocol exchange.

**Prefer cooperative assignors.** The `cooperative-sticky` strategy turns a full stop-the-world event into a graceful migration. This is the single highest-impact configuration change you can make.

Verify your assignor is configured correctly

kafka-consumer-groups.sh \ --bootstrap-server broker1:9092 \ --group my-group \ --describe \ --verbose


The verbose output shows the assigned partitions per consumer and the assignor in use.

## Observing Rebalances in Production

You need visibility before you can fix rebalance problems. Kafka does not emit structured logs for every rebalance event by default. Enable consumer-level metrics and log the join/sync phases.

// Java consumer metrics for rebalance observation Properties props = new Properties(); props.put(ConsumerConfig.GROUP_ID_CONFIG, "my-group"); props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "broker1:9092"); props.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG, "org.apache.kafka.clients.consumer.CooperativeStickyAssignor"); props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "60000"); props.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, "10000"); props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, "600000"); props.put(ConsumerConfig.METRICS_RECORDER_LOG_INTERVAL_SECONDS_CONFIG, "60");

KafkaConsumer<String, byte[]> consumer = new KafkaConsumer<>(props); consumer.subscribe(List.of("my-topic"));


Watch these metrics on your dashboard:

- `kafka.consumer:type=consumer-coordinator-metrics,client-id=*:rebalance-latency-avg`
- `kafka.consumer:type=consumer-coordinator-metrics,client-id=*:rebalance-latency-max`
- `kafka.consumer:type=consumer-coordinator-metrics,client-id=*:rebalance-rate-per-hour`
- `kafka.consumer:type=consumer-coordinator-metrics,client-id=*:records-lag-max`

A spike in `rebalance-rate-per-hour` means something is wrong. Your consumers should stabilize within minutes of deployment. Sustained rebalances indicate a timeout misconfiguration or a processing bottleneck.

## The Hard Trade-off

There is no configuration that eliminates rebalances entirely. Every scaling event triggers one. Every consumer restart triggers one. The question is how much downtime you accept.

Full rebalance under `range` or `roundrobin` stops all consumption. For a topic producing 50,000 records per second, a 10-second rebalance means 500,000 records pile up in the group. Your downstream systems see a sudden stall. This is unacceptable for real-time pipelines.

Cooperative rebalancing reduces the window to under 2 seconds in typical configurations. The remaining downtime is acceptable for most workloads. You pay a slightly higher join latency and a need to set longer timeouts. The timeouts themselves introduce a trade-off: slower detection of truly failed consumers.

The honest conclusion is that rebalances are a resource allocation problem, not a correctness problem. Your system will survive them if you design for the pause. Commit offsets explicitly. Make your processing functions idempotent. Run cooperative sticky assignors. Watch the metrics. Then go about your day without waking up to a rebalance page.

Latest posts