The question this answers
A consumer restarted and my whole group stopped for 20 seconds. Why does one member leaving affect everyone?
After a rebalance completes, every partition of the subscribed topics is assigned to exactly one member of the group. During the rebalance, no member of the group is processing. Across the boundary, records processed but not committed before revocation will be processed again by the new owner — the exclusivity guarantee holds per epoch, not across epochs.
Everything below is bought to hold this sentence. "Strongly consistent" with no scope attached is a slogan, not a guarantee — read what it actually covers, and what it explicitly does not.
The coordinator knows only that a member has not heartbeated within the session timeout. It cannot tell a crashed member from a paused, GC-stalled or slow one — Crashed or Just Slow: The Distinction You Cannot Make again, now with the whole group as the blast radius. A member, symmetrically, does not know its assignment has been revoked until it next talks to the coordinator, so it may be committing work for partitions someone else already owns.
A node knows its own state and the messages that arrived. Everything else is inference from evidence that was already stale. "B has not replied in five seconds" is knowledge; "B is down" is a decision — and usually the bug.
Why it is stop-the-world
Partition assignment must be mutually exclusive: two members processing the same partition means duplicate work and incoherent offsets. To guarantee exclusivity while the mapping changes, the classic protocol takes the simplest safe route — everyone releases everything, then everyone gets a new assignment. No member holds a partition during the transition, so no overlap is possible.
The cost is that the entire group stops, including the members whose assignments will not change at all. A twelve-member group where one pod restarts pauses all twelve. Depending on protocol and configuration this is typically seconds; with large groups, many partitions, or slow revocation handlers it can be tens of seconds, and every second is backlog accumulating on every partition.
This is a real distributed-agreement cost, and it is exactly what a work queue does not have. A queue worker dying affects only its own in-flight messages; a group member dying affects everyone. That difference is the price of the assignment guarantee, and it is the strongest argument for a queue when ordering and replay are not required.
The duplicate window at the boundary
Rebalancing produces duplicates by construction, and understanding exactly where they come from removes a great deal of confusion. A member has processed records up to offset 500 on partition 2, but has only committed offset 450 — commits are periodic, not per record. The rebalance revokes partition 2, and a new owner starts from 450. Records 451 to 500 are processed a second time.
The size of that window is the distance between "processed" and "committed", which is directly under your control through commit frequency — and is precisely the trade-off in Commit Before or After: There Is No Third Option. Commit more often and the window shrinks at the cost of throughput; commit less often and the window grows.
The revocation callback is where you shrink it deliberately: on revoke, stop fetching, finish or abandon in-flight work, and commit the true last-completed offset before releasing the partition. A group that does this well has a duplicate window of nearly zero on planned restarts. A group that ignores the callback reprocesses up to a full commit interval on every deploy, forever, and blames the broker.
Note the harder case the callback cannot fix: a member that was declared dead because it was *slow* never runs its revocation handler in time. It is still processing records for a partition the coordinator has already given away, and it may commit an offset for a partition it no longer owns. Some implementations reject that commit — which is a fencing check, and it is the only thing standing between you and two members writing incoherent positions.
| Trigger | Frequency | Cost | Preventable by |
|---|---|---|---|
| Deploy / rolling restarttypical | Every release | One pause per member restarted | Static membership; graceful leave; cooperative protocol |
| Autoscale up or downtypical | Continuous, if aggressive | A pause per scaling event | Scaling cooldowns; scaling to partition count and stopping |
| Processing exceeds poll intervalprotocol | Under load — exactly when it hurts | Pause, plus reprocessing, plus more load | Smaller batches; longer max poll interval; background heartbeat |
| GC pause or CPU starvationtypical | Sporadic | Same as above, harder to attribute | Heap tuning; heartbeat on a dedicated thread |
| Network blip to coordinatorassumption | Rare | Full rebalance for a healthy member | Longer session timeout, traded against slower failure detection |
| Topic metadata changeprotocol | Rare | Full rebalance across the group | Nothing — it is inherent |
The rebalance storm
Here is the loop, and it is worth memorising because it explains a large fraction of "our stream processor fell over and we do not know why" incidents.
A member is considered alive only if it keeps talking to the coordinator. In protocols where liveness is tied to how often the member asks for records, a member that spends too long processing a batch stops asking, is declared dead, and its partitions are given away. It then rejoins, triggering another rebalance. Meanwhile the pause has increased everyone’s backlog, so the next batch each member fetches is larger, so processing takes longer, so another member exceeds the interval. Now two members are cycling. Then three.
The signature is unmistakable once you have seen it: rebalances every few seconds, lag climbing steadily, CPU high, and completed-work throughput near zero. The group is spending all its time agreeing on who owns what and none of its time processing. It does not recover on its own, because the mechanism that would let it catch up is the mechanism being interrupted.
The fixes are all about decoupling liveness from processing duration: heartbeat on a separate thread so liveness does not depend on the handler; cap the number of records per fetch so a batch cannot take too long; raise the maximum processing interval to something safely above your p99 batch time; and hand very long work to a side queue rather than blocking the consumer loop. This is the same conclusion as Visibility Timeout: The Message Is Hidden, Not Yours reached by a different route — never let "how long the work takes" be the signal for "is this process alive".
09:14:02 Attempt to heartbeat failed: group is rebalancing
09:14:02 Revoke partitions [3, 7, 11]
09:14:09 Assigned partitions [3, 7, 11, 14] (7s pause)
09:14:41 Member consumer-4 failed: poll interval exceeded (processed 500
records in 312s, max.poll.interval.ms = 300000)
09:14:41 Revoke partitions [3, 7, 11, 14]
09:14:50 Assigned partitions [1, 3, 7, 11, 14] (9s pause, MORE partitions)
09:15:29 Member consumer-2 failed: poll interval exceeded
...
Throughput: ~0. Lag: climbing. CPU: 85%. No handler errors anywhere.
Each pause increases every member's batch size, which causes the next timeout.Protocols that make it hurt less
Two mitigations are worth understanding structurally, because they attack different halves of the problem.
Cooperative / incremental rebalancing removes the stop-the-world property. Rather than everyone revoking everything, the assignment is computed as a diff and only the partitions that actually move are revoked. A member whose assignment is unchanged never stops. This turns a group-wide pause into a per-partition handover and is a large improvement for big groups — at the cost of a more complex protocol, sometimes two rounds, and behaviour that is harder to reason about during an incident.
Static membership removes many rebalances entirely. Each member carries a stable identity across restarts, and the coordinator holds its assignment for a grace period rather than reassigning immediately. A pod that restarts within the window gets its own partitions back and no rebalance occurs. This is close to free for rolling deploys and is the highest-value setting most teams have not turned on. The trade is that a genuinely dead member’s partitions are unowned for the whole grace period, so you have chosen slower failure detection in exchange for fewer spurious rebalances — the same dial as every failure detector in No Heartbeat Does Not Mean Dead.
Neither removes the duplicate window. That belongs to the commit point, and no rebalancing protocol can fix a decision made in Commit Before or After: There Is No Third Option.
Key points
- A membership change redistributes partitions; in the classic protocol every member revokes everything, so the whole group pauses.
- Records processed after the last commit are reprocessed by the new owner — the duplicate window is the gap between processed and committed.
- Long processing makes a member look dead, which triggers a rebalance, which increases everyone’s batch size, which causes more timeouts: the rebalance storm.
- Static membership eliminates most deploy-time rebalances; cooperative rebalancing removes the stop-the-world pause. They solve different halves.
- Never let processing duration be the liveness signal — heartbeat independently and cap batch size.
The chain, answered
Every field here is required, which is why no lesson in this domain can recommend a design without naming what an operator sees when it fails, what survives the partition, what repairs it afterwards, and the simpler thing to consider first.
- • A member joins, leaves, fails to heartbeat, or topic metadata changes; the coordinator marks the group as rebalancing.
- • Under eager rebalancing, all members revoke all partitions and run their revocation callbacks; under cooperative rebalancing, only moving partitions are revoked.
- • Members rejoin; a leader computes the new assignment using the configured strategy.
- • The assignment is distributed and members resume from the last committed offset for each newly held partition.
- • Any records processed after that committed offset by the previous owner are processed again.
- • A slow member is declared dead and its partitions are reassigned while it is still processing them.
- • The revocation callback is not implemented, so uncommitted work is silently reprocessed on every rebalance.
- • The revocation callback is slow, extending the group-wide pause for everyone.
- • A member commits an offset for a partition it no longer owns.
- • Repeated rebalances prevent any member from making enough progress to stop triggering them.
- • Rebalance storm: the operator sees rebalance events every few seconds, lag climbing monotonically, CPU at 85%, and completed-record throughput near zero. There are no handler exceptions, so error-rate dashboards are clean and the incident looks like an infrastructure failure.
- • Deploy-time backlog spike: every rolling deploy produces a step increase in lag proportional to the number of pods times the pause duration. Teams learn to deploy at night and never diagnose it.
- • Duplicate wave on every restart: downstream sees a burst of duplicate records exactly at each deploy, sized by the commit interval. Handlers that are idempotent absorb it invisibly; handlers that are not produce a small, regular stream of data errors.
- • Zombie member: a GC-stalled member wakes up, processes a batch and commits for partitions another member now owns. The operator sees an offset going backwards, or a commit rejection, and two members logging work for the same partition.
- • Cascading timeout after a dependency slows: a downstream API doubles its latency, batch processing exceeds the poll interval, and the group enters a storm. The root cause is external and the symptom is entirely internal, which sends the investigation in the wrong direction.
- • A rebalance is a small consensus round: the group must agree on membership and on a single assignment. That agreement is what guarantees exclusivity and is what costs the pause.
- • The trade sits on the No Heartbeat Does Not Mean Dead dial: short timeouts detect real failures fast and generate spurious rebalances; long timeouts avoid spurious rebalances and leave partitions unowned longer after a real failure.
- • Cooperative rebalancing reduces the *scope* of the agreement rather than its existence — fewer participants change state, so fewer are blocked.
- • After any rebalance, every partition has exactly one owner; assignment completeness is preserved even when members are lost.
- • No committed progress is lost; the group resumes from committed offsets, so failure costs duplicates rather than gaps.
- • Exclusivity is not preserved across the boundary: a slow previous owner may still be processing records the new owner is also processing.
- • Detect: rebalance rate per group as a first-class metric, alongside lag. A group rebalancing more than once per deploy is unhealthy.
- • Contain: to break a storm, reduce batch size and raise the maximum processing interval — both reduce the trigger immediately. Adding members makes it worse.
- • Recover: let the group stabilise before scaling; verify rebalance rate returns to zero before evaluating lag.
- • Reconcile: expect and absorb the duplicate wave; verify downstream deduplication handled it rather than assuming it did.
- • Verify: rebalance rate at zero in steady state, per-partition lag draining, and every partition owned.
- • Rebalance count and duration per group — the metric that names this failure directly, and the one most often absent.
- • Time spent processing per batch (p99) against the maximum poll interval; the ratio is the storm risk.
- • Heartbeat failures and session timeouts, separately from processing errors.
- • Duplicate-processing rate around deploys, which measures the commit-window cost.
- • Assignment churn per partition: a partition changing owner frequently is a group that is not settling.
- • Automatic failover: a dead member’s partitions are picked up without operator action, which is the whole reason the mechanism exists.
- • Elastic scaling up to partition count, where a new member takes a share of the work with no configuration change.
- • Rolling deploys, which work at all only because reassignment is automatic.
- • Long or highly variable processing times, which turn liveness detection into a false-positive generator.
- • Large groups with many partitions, where the pause is long and every membership change is expensive.
- • Aggressive autoscaling, which converts a helpful mechanism into a continuous one.
- • Consumers holding expensive in-memory state per partition, where every reassignment means rebuilding that state.
- • Static membership with a generous grace period, removing rebalances for planned restarts entirely.
- • Cooperative / incremental rebalancing, removing the group-wide pause so only moving partitions stop.
- • Manual partition assignment: pin members to partitions and skip group coordination altogether. No rebalances, no automatic failover — you own the failover.
- • A work queue, if the ordering and replay guarantees are not needed. A worker dying costs its in-flight messages, not the whole pool’s throughput.
Everyone stops so the partitions can move
What people believe, and what is true
Rebalancing only affects the consumer that left.
In the classic protocol every member revokes everything, so the entire group stops — including members whose assignment does not change.
Rebalances are rare.
Every deploy, every autoscale event, every slow batch and every network blip triggers one. In many production groups they are the dominant source of duplicate processing.
A consumer that is slow is still healthy.
The coordinator cannot see slow — it sees silence. A slow member is indistinguishable from a dead one, and gets treated as dead.
Adding consumers will help the group catch up.
Each addition triggers a rebalance. During a storm, adding members deepens the storm and adds nothing past partition count.
Go deeper
Only the levels this lesson can honestly fill — a missing level is a claim nobody had.
Overview
When a consumer joins or leaves, the group redistributes partitions and pauses while it does. Work processed but not committed gets done again by the new owner.
Practical
Turn on static membership so deploys stop causing rebalances. Use cooperative rebalancing if available. Cap records per fetch and set the maximum processing interval above p99 batch time. Commit in the revocation callback. Alert on rebalance rate, not only on lag.
Advanced
The storm is a positive feedback loop with the same structure as One Retry per Tier Is Not One Retry — It Multiplies: the system’s response to overload consumes the capacity needed to recover from it. Each rebalance costs processing time, which increases backlog, which increases batch size, which increases the chance of the next timeout. The loop has no stable fixed point under load, which is why it does not self-heal and why adding capacity makes it worse. Breaking it means reducing the *trigger* rather than adding resources — smaller batches, longer intervals, liveness decoupled from work — which is the general shape of every fix for a congestion collapse.
Apply it
- 🔧 Set the maximum poll interval below your p99 batch time and watch the group enter a storm. Then fix it with batch size alone and confirm the storm ends.
- 🔧 Implement a revocation callback that commits the true last-completed offset per partition, and measure the reduction in duplicates across a deploy.
- ⚡ A downstream API doubles its latency and your stream processor stops entirely with no errors. Trace the causal chain and name the two settings you would change first.
- ⚡ Every rolling deploy produces a lag step and a burst of duplicate rows downstream. Give the two changes that address each symptom separately.
- 💬 One consumer restarts and the whole group stops. Why?
- 💬 Describe a rebalance storm: the trigger, the loop, and why adding consumers makes it worse.
- 💬 Where exactly do the duplicates at a rebalance boundary come from, and how do you shrink the window?