Consumer Patterns: Groups, Lag, Backpressure, and Rebalancing
Consumer lag is a diagnostic symptom, not a problem to solve by adding more consumers. This article teaches how to diagnose lag root causes — slow consumer logic, downstream DB backpressure, GC pause cascades, and rebalance storms — and applies the correct fix for each. It also covers cooperative sticky rebalancing, manual offset commit patterns, and RabbitMQ prefetch-based backpressure.
Distributed Messaging Systems
Consumer Patterns: Groups, Lag, Backpressure, and Rebalancing
The on-call alert fires at 11 p.m.: consumer lag on orders.created has crossed 500,000 messages and the SLO for fulfillment dispatch is 60 seconds from order creation. The engineer opens the consumer group dashboard, sees 4 running instances, and scales to 12. Lag continues growing. The topic has 4 partitions. The extra 8 instances are idle — Kafka has nothing to assign them. The actual cause is a downstream PostgreSQL write that degraded from 5ms to 340ms two hours ago when a batch job saturated the primary's I/O. The fix is not more consumers; it is fixing the database or applying backpressure at the consumer level.
Consumer lag is a symptom, not a problem. The root cause determines the correct fix — and adding consumer instances is almost never it.
Series positioning: This is Part 6 of Distributed Messaging Systems. Part 5 established delivery guarantees and idempotent consumption. This article focuses on the consumer lifecycle: partition assignment, lag diagnosis, backpressure, and rebalance mechanics. The prerequisite mental model for partition-based parallelism is in Scaling: Partitioning, Sharding, and Replication.
1. Consumer Group Parallelism: The Hard Cap
1.1 Partition Assignment is One-to-One
A Kafka partition can be assigned to at most one consumer instance within a consumer group at any point in time. Parallelism is bounded by partition count — this is not a configuration; it is the data model.
| Consumers | Partitions | Active | Idle | Parallelism |
|---|---|---|---|---|
| 3 | 6 | 3 | 0 | 3 (each handles 2 partitions) |
| 6 | 6 | 6 | 0 | 6 (maximum) |
| 12 | 6 | 6 | 6 | 6 (no gain — 6 idle) |
| 6 | 12 | 6 | 0 | 6 (each handles 2 partitions, headroom to scale) |
Provision partitions for your expected peak consumer count, not your current one. Partition counts can be increased but never decreased — plan 2–4× headroom. A topic you create today with 6 partitions is capped at 6-way parallelism forever unless you recreate it.
1.2 Partition Count and Ordering
Increasing partition count trades ordering scope for parallelism:
2. Diagnosing Consumer Lag
2.1 The Four Root Causes
Consumer lag (offset delta between the log head and the consumer's committed offset) has four distinct root causes, each requiring a different fix:
| Root Cause | Lag Pattern | Symptom | Fix |
|---|---|---|---|
| Slow consumer logic | Steady, linear growth | CPU high, processing time per message rising | Optimize handler (batching, caching, async I/O) |
| Downstream backpressure | Spiky, correlated with downstream metrics | DB/API latency rising, consumer threads blocked | Circuit breaker, reduce max.poll.records, fix downstream |
| Rebalance storms | Sawtooth pattern (lag drops then spikes repeatedly) | Frequent group rebalances in broker logs | Increase max.poll.interval.ms, fix poll() starvation |
| Insufficient partition count | Lag grows despite healthy consumers | All partitions assigned, processing rate < produce rate | Increase partitions, scale consumers to match |
2.2 Measuring Lag
Missing heartbeat() inside a long batch processing loop is the most common cause of rebalance storms. The consumer must call poll() (or heartbeat() in kafkajs eachBatch) within max.poll.interval.ms (default 5 minutes). A 100,000-message batch that takes 8 minutes to process will trigger a session timeout, a rebalance, and redelivery of the entire batch.
3. Backpressure: Slowing the Consumer to Protect the Downstream
3.1 max.poll.records as a Backpressure Valve
3.2 RabbitMQ Prefetch as Backpressure
In RabbitMQ, channel.prefetch() is the backpressure mechanism — the broker will not push more messages until the in-flight count drops below the limit:
| Prefetch | In-flight messages | DB throughput target | Behaviour |
|---|---|---|---|
1 |
1 at a time | < 50 msg/s | Safe; serial; low throughput |
10 |
up to 10 | 100–500 msg/s | Balanced |
50 |
up to 50 | 500–2,000 msg/s | High throughput, more memory |
0 (none) |
unlimited | — | ❌ Broker floods consumer |
4. Rebalancing: Stop-the-World vs Cooperative
4.1 Eager (Stop-the-World) Rebalancing
The default eager rebalancing protocol revokes all partition assignments when any consumer joins or leaves, forces all consumers to stop processing, waits for re-assignment, then resumes. For 20 consumers with 100ms processing latency, this is a 2–5 second dead stop every time a consumer deploys.
4.2 Cooperative Sticky Rebalancing
The CooperativeStickyAssignor (Kafka 2.4+) only revokes partitions that need to move. Unaffected consumers continue processing throughout the rebalance:
4.3 Rebalance Storm Prevention
The three-timeout relationship: heartbeatInterval < sessionTimeout / 3. If sessionTimeout=30s and heartbeatInterval=15s, a single missed heartbeat causes an immediate session timeout. Default values (sessionTimeout=30s, heartbeatInterval=3s) are correct for fast consumers — increase sessionTimeout and maxWaitTimeInMs proportionally when processing is deliberately slow.
5. Batch Processing vs Per-Message: When to Use Each
| Mode | Throughput | Heartbeat | DB round-trips | Best for |
|---|---|---|---|---|
eachMessage |
Lower | Automatic | 1 per message | Simple pipelines, < 1,000 msg/s |
eachBatch |
Higher | Manual (required) | 1 per batch | Bulk inserts, analytics, > 5,000 msg/s |
Summary
| Concept | Rule |
|---|---|
| Parallelism cap | Consumer group parallelism is hard-capped at partition count — adding consumers beyond that count is waste, not scale. |
| Cooperative rebalancing | Cooperative Sticky Assignor eliminates stop-the-world rebalances; use it by default on all Kafka client versions that support it. |
| Lag root cause | Lag root cause determines fix: lag from slow logic → optimize consumer; lag from downstream → apply backpressure; lag from rebalance storms → increase session.timeout.ms and max.poll.interval.ms. |
What's Next
Part 7: Request-Reply over Messaging — Correlation IDs, Temporary Queues, and When Not To covers the cases where a caller genuinely needs a response from a downstream service but the synchronous HTTP call is not available or not appropriate. The correlation ID pattern, reply-to queues, and the timeout contract make request-reply over messaging safe — but most teams should reach for HTTP/gRPC first.
This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.