Blog
Splitting Work Without Losing Order: Consumer Groups Demystified
Consumer groups are how Kafka turns one ordered log into a horizontally scalable workload, and the partition-to-consumer mapping decides everything about your throughput and ordering.
Once you accept that Kafka is a partitioned log, the obvious next question is how multiple worker instances share the load without reading the same records twice. The answer is the consumer group, and it is one of the most elegant and most misunderstood parts of Kafka. A consumer group is a set of consumer instances that cooperatively read a topic, with Kafka guaranteeing that each partition is consumed by exactly one member of the group at a time. This gives you a clean way to scale out: add more consumers and Kafka redistributes partitions among them. But the relationship between partition count and consumer count creates hard limits and subtle ordering effects that trip up developers used to spinning up arbitrary numbers of Express workers. Getting this model right is the difference between linear scaling and idle, starved consumers doing nothing.
One Partition, One Consumer
The foundational rule is that within a single consumer group, each partition is assigned to at most one consumer. Two consumers in the same group will never read the same partition simultaneously. This is what prevents duplicate processing across your worker fleet. The immediate consequence is that the number of partitions sets the ceiling on parallelism for that group. If a topic has six partitions, you can usefully run up to six consumers; a seventh consumer in the group sits idle with no partition to read. This is unlike scaling Express servers behind a load balancer, where adding instances always helps. In Kafka, scaling consumers beyond partition count is wasted capacity. Therefore you provision partitions at topic creation with future scale in mind, since increasing partitions later is possible but disruptive to keyed ordering. Think of partitions as the real concurrency budget.
Multiple Groups Read Independently
While one partition maps to one consumer within a group, different consumer groups are completely independent of each other. Each group maintains its own set of committed offsets for the same topic. This is the mechanism behind Kafka's broadcast capability. Your billing service runs as group billing, your analytics service as group analytics, and both read every message in the topic at their own pace without interfering. One group can be hours behind processing a backlog while another stays caught up in real time. Coming from RabbitMQ, this feels like the difference between a work queue and a publish-subscribe fanout, except Kafka gives you both simultaneously over the same durable log. When you design a system, the group id is your declaration of intent: same group means share the work, different group means everyone gets a full copy of the stream.
Offsets And Where They Live
Each consumer group tracks how far it has read by committing offsets, which Kafka stores in an internal compacted topic called __consumer_offsets. When a consumer commits offset 500 for a partition, it is recording that records up to 499 are done and the next poll should start at 500. If the consumer restarts, it resumes from the last committed offset rather than the beginning. You control commit timing: automatic commits happen periodically in the background, while manual commits let you commit only after you have successfully processed a record. The choice matters enormously for delivery semantics. Auto-commit is convenient but can acknowledge work you have not actually finished if you crash mid-processing. For a backend developer, the offset is your bookmark, and deciding exactly when to move that bookmark is how you trade simplicity for correctness under failure.
Rebalancing And Its Costs
When a consumer joins or leaves a group, or when partitions change, Kafka triggers a rebalance, reassigning partitions among the current members. During a classic rebalance, all consumers briefly stop processing, revoke their partitions, and wait for the new assignment, a pause sometimes called a stop-the-world rebalance. For high-throughput systems this pause is painful, so newer Kafka versions offer cooperative incremental rebalancing, which reassigns only the partitions that actually need to move while others keep processing. Rebalances are also triggered unintentionally when a consumer is too slow and misses its heartbeat or exceeds max.poll.interval.ms, causing the group to think it died. A common production bug is processing taking longer than the poll interval, kicking the consumer out and triggering endless rebalances. Tuning poll size and processing time to avoid spurious rebalances is a recurring operational concern.
Heartbeats, Sessions, And Liveness
Kafka decides a consumer is alive through two separate mechanisms that developers often confuse. A background heartbeat thread regularly tells the group coordinator the consumer is healthy, governed by session.timeout.ms; miss enough heartbeats and you are evicted. Separately, max.poll.interval.ms bounds how long you may take between calls to poll, ensuring a consumer that is technically alive but stuck processing a record forever gets removed. The split exists because heartbeating runs independently of your processing loop, so a consumer can keep heartbeating while being hopelessly behind. The practical guidance is to keep per-batch processing well under the poll interval, and if work is genuinely long, reduce max.poll.records so each batch is smaller. This mirrors keeping request handlers fast in Express so the event loop stays responsive, except here the penalty for blocking is being ejected from the group entirely.
Keys, Ordering, And Hot Partitions
Because ordering is guaranteed only within a partition, and each partition goes to one consumer in a group, the partition key determines both your ordering and your load distribution. Keying by user-id means all of one user's events are processed in order by a single consumer, which is usually what you want. But if one key is wildly more active than others, say a celebrity account or a single huge tenant, that partition becomes a hot spot. One consumer drowns while others idle, and you cannot fix it by adding consumers because that partition still maps to one. Solutions include using a composite key to spread load, accepting weaker ordering for hot keys, or isolating large tenants into dedicated topics. This is the same skew problem as a bad shard key in Mongo, and the cure is always thoughtful key design rather than more hardware.
Static Membership For Stability
Rolling deployments are a classic source of rebalance churn: every time a pod restarts, the group sees a member leave and rejoin, triggering reassignment twice. Kafka addresses this with static group membership, where each consumer is given a stable group.instance.id. With static membership, a consumer that disconnects and reconnects within the session timeout reclaims its previous partitions without forcing a full rebalance, because the coordinator recognizes it as the same logical member returning. This dramatically smooths deployments and transient network blips for stateful consumers that are expensive to reassign. For someone running consumers in Kubernetes, pairing static membership with a stable identity per pod is a standard reliability practice. It will not eliminate rebalances when you genuinely change the consumer count, but it stops routine restarts from disrupting a healthy group, which is often the noisiest source of latency spikes in production.
Sizing Partitions Up Front
All of this converges on one design decision you make early and regret late: how many partitions a topic should have. Too few, and you cap your maximum consumer parallelism, leaving you unable to scale out under load. Too many, and you pay overhead in open file handles, replication traffic, longer rebalances, and more end-to-end latency, since each partition is a unit of coordination. A reasonable approach is to estimate your peak required throughput, divide by the throughput a single consumer can sustain, and add headroom for growth. Remember that increasing partitions later changes how keys hash to partitions, breaking the ordering guarantee for existing keys, so it is not a casual operation. Treat partition count like a schema decision rather than a runtime tuning knob, and you will avoid the most common scaling wall teams hit months into running Kafka.