---
title: Consumer Groups and Collaboration
url: https://doc.liz6.com/en/distributed-systems/07-messages-and-streams/03-consumer-groups-and-collaboration
locale: en
area: distributed-systems
tags:
- distributed-systems
- messages-and-streams
date: 2026-06-30
modified: 2026-07-16
description: A consumer group is a negotiation among consumers for partition ownership—an essential variant of distributed consensus. Eager vs. cooperative rebalancing determines whether the entire group stops or continues consuming during rebalancing.
---

# Consumer Groups and Collaboration

> A consumer group is a negotiation among consumers for partition ownership—an essential variant of distributed consensus. Eager vs. cooperative rebalancing determines whether the entire group stops or continues consuming during rebalancing.

## Overview

[Kafka Architecture](/distributed-systems/07-messages-and-streams/02-kafka-architecture.md) discussed partitions and ISR—distributed mechanisms at the storage/replication layer. However, the consumer side also faces distributed coordination challenges: when multiple consumers share partitions of a topic, who consumes which partition? How are partitions reassigned if a consumer fails? Does consumption pause during reassignment? These questions are addressed by the **consumer group**—essentially, a "negotiation among a group of consumers for partition ownership," which is a true variant of distributed consensus.

## How Consumer Groups Work

Kafka's consumption model: **A partition can be consumed by only one consumer within a consumer group.** This is the prerequisite for guaranteeing "message ordering within a partition." If two consumers read the same partition simultaneously, their respective offset ranges will be discontinuous, breaking the order.

<svg viewBox="0 0 720 340" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="Comparison before and after consumer group assignment: 6 partitions assigned to 3 consumers">
  <defs>
    <marker id="cga1" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker>
  </defs>
  <rect width="720" height="340" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">Consumer Group Assignment: 6 Partitions Assigned to 3 Consumers</text>
  <line x1="360" y1="44" x2="360" y2="290" stroke="#e2e8f0" stroke-width="1"/>
  <text x="190" y="58" text-anchor="middle" font-size="13" font-weight="700" fill="#64748b">Before Assignment (During Rebalance)</text>
  <text x="530" y="58" text-anchor="middle" font-size="13" font-weight="700" fill="#4f46e5">After Assignment</text>

  <rect x="52" y="70" width="36" height="40" rx="4" fill="#e2e8f0"/>
  <text x="70" y="86" text-anchor="middle" font-size="11" font-weight="600" fill="#475569">P0</text>
  <text x="70" y="102" text-anchor="middle" font-size="10" fill="#94a3b8">??</text>
  <rect x="92" y="70" width="36" height="40" rx="4" fill="#e2e8f0"/>
  <text x="110" y="86" text-anchor="middle" font-size="11" font-weight="600" fill="#475569">P1</text>
  <text x="110" y="102" text-anchor="middle" font-size="10" fill="#94a3b8">??</text>
  <rect x="132" y="70" width="36" height="40" rx="4" fill="#e2e8f0"/>
  <text x="150" y="86" text-anchor="middle" font-size="11" font-weight="600" fill="#475569">P2</text>
  <text x="150" y="102" text-anchor="middle" font-size="10" fill="#94a3b8">??</text>
  <rect x="172" y="70" width="36" height="40" rx="4" fill="#e2e8f0"/>
  <text x="190" y="86" text-anchor="middle" font-size="11" font-weight="600" fill="#475569">P3</text>
  <text x="190" y="102" text-anchor="middle" font-size="10" fill="#94a3b8">??</text>
  <rect x="212" y="70" width="36" height="40" rx="4" fill="#e2e8f0"/>
  <text x="230" y="86" text-anchor="middle" font-size="11" font-weight="600" fill="#475569">P4</text>
  <text x="230" y="102" text-anchor="middle" font-size="10" fill="#94a3b8">??</text>
  <rect x="252" y="70" width="36" height="40" rx="4" fill="#e2e8f0"/>
  <text x="270" y="86" text-anchor="middle" font-size="11" font-weight="600" fill="#475569">P5</text>
  <text x="270" y="102" text-anchor="middle" font-size="10" fill="#94a3b8">??</text>
  <text x="190" y="130" text-anchor="middle" font-size="11" fill="#94a3b8">Consumers currently hold no partitions</text>

  <rect x="65" y="150" width="70" height="30" rx="6" fill="#f8fafc" stroke="#cbd5e1" stroke-dasharray="4 3"/>
  <text x="100" y="169" text-anchor="middle" font-size="12" fill="#94a3b8">C1</text>
  <rect x="155" y="150" width="70" height="30" rx="6" fill="#f8fafc" stroke="#cbd5e1" stroke-dasharray="4 3"/>
  <text x="190" y="169" text-anchor="middle" font-size="12" fill="#94a3b8">C2</text>
  <rect x="245" y="150" width="70" height="30" rx="6" fill="#f8fafc" stroke="#cbd5e1" stroke-dasharray="4 3"/>
  <text x="280" y="169" text-anchor="middle" font-size="12" fill="#94a3b8">C3</text>
  <text x="190" y="200" text-anchor="middle" font-size="11" fill="#94a3b8">Waiting for the group coordinator to calculate the new assignment</text>

  <rect x="380" y="72" width="36" height="28" rx="4" fill="#0d9488"/>
  <text x="398" y="91" text-anchor="middle" font-size="10" fill="#ffffff">P0</text>
  <rect x="420" y="72" width="36" height="28" rx="4" fill="#0d9488"/>
  <text x="438" y="91" text-anchor="middle" font-size="10" fill="#ffffff">P1</text>
  <line x1="460" y1="86" x2="492" y2="86" stroke="#475569" stroke-width="1.6" marker-end="url(#cga1)"/>
  <rect x="496" y="72" width="150" height="28" rx="6" fill="#4f46e5"/>
  <text x="571" y="91" text-anchor="middle" font-size="13" font-weight="700" fill="#ffffff">C1</text>

  <rect x="380" y="120" width="36" height="28" rx="4" fill="#0d9488"/>
  <text x="398" y="139" text-anchor="middle" font-size="10" fill="#ffffff">P2</text>
  <rect x="420" y="120" width="36" height="28" rx="4" fill="#0d9488"/>
  <text x="438" y="139" text-anchor="middle" font-size="10" fill="#ffffff">P3</text>
  <line x1="460" y1="134" x2="492" y2="134" stroke="#475569" stroke-width="1.6" marker-end="url(#cga1)"/>
  <rect x="496" y="120" width="150" height="28" rx="6" fill="#4f46e5"/>
  <text x="571" y="139" text-anchor="middle" font-size="13" font-weight="700" fill="#ffffff">C2</text>

  <rect x="380" y="168" width="36" height="28" rx="4" fill="#0d9488"/>
  <text x="398" y="187" text-anchor="middle" font-size="10" fill="#ffffff">P4</text>
  <rect x="420" y="168" width="36" height="28" rx="4" fill="#0d9488"/>
  <text x="438" y="187" text-anchor="middle" font-size="10" fill="#ffffff">P5</text>
  <line x1="460" y1="182" x2="492" y2="182" stroke="#475569" stroke-width="1.6" marker-end="url(#cga1)"/>
  <rect x="496" y="168" width="150" height="28" rx="6" fill="#4f46e5"/>
  <text x="571" y="187" text-anchor="middle" font-size="13" font-weight="700" fill="#ffffff">C3</text>

  <rect x="60" y="300" width="600" height="30" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="360" y="320" text-anchor="middle" font-size="12.5" fill="#3730a3">A partition can be consumed by only one consumer within a group—6 partitions determine the upper limit of parallelism; a 7th consumer joining will remain idle.</text>
</svg>

Key point: The number of partitions determines the upper limit of parallelism. 6 partitions → maximum 6 consumers; if a 7th consumer joins, it will be **idle** (no partitions assigned to it). **To increase consumption parallelism, you must add partitions, not consumers.**

## Rebalancing: Negotiating Ownership

When a consumer joins or leaves a group, a rebalance is triggered to reassign partition ownership. This is the most critical and error-prone part of the consumer group mechanism.

### Eager Rebalance (Old Protocol): Stop and Reassign

```
1. New consumer C4 joins → Group coordinator notifies all consumers: release current assignments
2. All consumers stop consuming and release partition ownership
3. Reassignment: C1←P0,P1  C2←P2  C3←P3,P4  C4←P5
4. All consumers resume consumption from the last committed offset
```

This process is called **stop-the-world**: during steps 2-3, the **entire consumer group stops consuming**, leading to latency buildup. For latency-sensitive stream processing applications, every rebalance is an incident.

### Cooperative Rebalance (New Protocol, KIP-429): Incremental

```
1. C4 joins → Coordinator calculates new assignment, notifying only consumers that "need to release partitions"
2. Affected consumers release the taken partitions but continue consuming unaffected partitions
3. New owners (C4) take over the released partitions
4. Throughout the process, unaffected consumers do not stop
```

The essential difference: eager makes everyone release and then reassign; cooperative only affects those impacted, leaving others running. **It changes from "everyone stops" to "some keep running,"** significantly reducing the business impact of rebalancing.

### Sticky Assignment: Maintain Existing Assignments Whenever Possible

Assignments after rebalancing consider not just "uniformity" but also "minimal change": if a consumer is already consuming a partition and does not need to release it, the assignment is **kept unchanged**. This is called **sticky**—avoiding unnecessary assignment changes and reducing consumer state switches.

## Offset Management: The "Persistent Pointer" of Consumption Progress

Where the consumer has read needs to be recorded—this is the offset. There are two management methods:

| | Auto Commit | Manual Commit |
|---|---|---|
| Timing | Automatically commits the current offset at time intervals (e.g., every 5 seconds) | Application explicitly calls `commitSync()` / `commitAsync()` |
| Risk | Process completes but commit interval hasn't arrived → crash → duplicate consumption (at-least-once) | Process completes → commit → crash → no duplicates; Process completes but crashes before commit → duplicates |
| Use Case | Tolerates small amounts of duplication | Requires fine-grained control (e.g., exactly-once consumption workflows) |

**Where offsets are stored:** Kafka's internal `__consumer_offsets` topic (multi-partition, with replication). A consumer group's offset commit is a message written to this topic—Kafka uses its own storage mechanism to persist consumption progress.

## Exactly-Once Consumption: Linking Consumer-Side Transactions

[Message Semantics](/distributed-systems/07-messages-and-streams/01-message-semantics.md) discussed producer-side exactly-once (idempotent producer + transactions). How is exactly-once achieved on the consumer side? The core is to place the **commitment of consumption offsets** and the **output of processing results** within the same transaction:

```
Consumer Transaction:
  1. Read message from topic-A (offset=N)
  2. Process → Write result to topic-B
  3. Commit offset (N+1) to __consumer_offsets
Steps 2 and 3 are within the same Kafka transaction:
    - Transaction success: Result written to topic-B, offset advances to N+1 → Consumption "occurred"
    - Transaction failure/Consumer crash: Result not written, offset rolls back to N → Message will be re-consumed
```

Key requirement: The consumer's `isolation.level` must be set to `read_committed`—only read messages from committed transactions, ignoring aborted transactions and incomplete dangling transactions.

This entire combination (idempotent producer + transactional producer + `read_committed` consumer + offset and output in the same transaction) constitutes Kafka's exactly-once semantics—not a theoretical "perfect once-only," but an engineering "effectively once-only," ensuring no duplicates or losses.

## Comparison with RabbitMQ: Queue Model vs. Log Model

Another approach in the same chapter: RabbitMQ uses **queues** rather than logs. Messages are **deleted** after acknowledgment (not appended to an immutable log), with no replay/backtracking and no global ordering within partitions. Consumer group coordination also differs: RabbitMQ consumers do not negotiate partition ownership; instead, the broker distributes messages via round-robin or consistent-hash exchange—simpler, but lacking Kafka's "same key to same partition guarantees order."

The choice of Kafka's log model is because distributed stream processing requires **replay** and **global ordering**—two things the queue model cannot do. A Kafka partition is essentially a sharded, immutable, replayable log, and the consumer group is the "who reads which shard" negotiation layer on top of this log.

## Common Issues and Fixes

- **Rebalance Storm**: Consumers frequently join/leave (e.g., heartbeat timeout threshold too short, processing time too long → considered dead). Each rebalance causes the entire group to pause. → Set `max.poll.interval.ms` large enough (greater than the longest processing time), use cooperative rebalancing, and monitor rebalance frequency.
- **Partition Count < Consumer Count**: Extra consumers remain idle → either add partitions or accept idle consumers (as hot backups).
- **Consumer Lag**: Consumer processing speed cannot keep up with producer write speed → monitor lag (consumer offset vs. partition end offset), add consumers or partitions.
- **Auto Commit + Async Processing**: Consumer submits offset after throwing message into thread pool → when async thread processing fails, the message is already "consumed" → loss. → Either process synchronously or manually commit after async processing completes.

## References

- **Kafka Documentation**: Consumer Group Protocol, KIP-429 (Incremental Cooperative Rebalancing)

*Keywords: consumer group, rebalance, eager, cooperative (incremental), sticky assignment, stop-the-world, group coordinator, offset management, __consumer_offsets, exactly-once consumption, isolation.level, read_committed, transactional consumer, consumer lag, rebalance storm, Kafka vs RabbitMQ, log vs queue*
