Partitions, rebalancing and the hot key that won't split.
A partition key is three decisions at once: an ordering boundary, a load distribution, and a scalability ceiling for that key. Almost every Kafka scaling problem is one of those three colliding with the other two.
00The 10-second model
Partitioning decides where a record lives and defines its ordering boundary. Consumer groups decide how partitions are divided among workers. Rebalancing changes that ownership. Hot partitions happen when load concentrates in one partition — usually because one key is far hotter than the rest.
“One partition → one consumer” always means one active consumer per consumer group. A different group consumes the same partition independently, with its own offsets.
01Partitions and ordering
A partition is an append-only log. Offsets are partition-local and monotonically increasing. Records are retained by policy even after consumption, which is what makes replay possible — consuming does not delete anything.
Choosing the key
| Key | Preserves | Risk |
|---|---|---|
| video_id | Total order per video | One celebrity video becomes one hot partition |
| video_id:user_id | Order per user within a video | No total order across users |
| video_id:shard_id | Controlled parallelism for a hot video | Needs shard routing and versioning |
| comment_id / random | Maximum distribution | No meaningful business ordering at all |
The question that settles it: what is the smallest unit for which the business genuinely requires ordering? Partition by that, and no finer.
02Consumer groups and the parallelism ceiling
Partition count is therefore a capacity-planning input, not an implementation detail. If you will need 40-way consumer parallelism later, a 4-partition topic cannot provide it without repartitioning — and over-partitioning has its own metadata and coordination costs, so it is a real trade-off rather than “pick a big number”.
Why one owner per group at all? Because concurrent consumers of the same partition would complete records out of order, and Kafka's model exists to make ordered consumption tractable. You can parallelise inside a consumer — but then ordering and offset bookkeeping become your problem, not Kafka's.
03Rebalancing
A group's membership changes; every subscribed partition still needs exactly one owner inside that group. Rebalancing is the protocol that converges from the old ownership map to a valid new one, coordinated by a broker acting as group coordinator (not the cluster controller — different job).
| Trigger | Why ownership must move |
|---|---|
| Consumer joins | New capacity available to take partitions. |
| Graceful leave | Its partitions need a new owner. |
| Crash / missed session | Coordinator decides the member is unhealthy. |
| Poll loop stalls | Alive on the network but not making progress — ownership revoked. |
| Topic / subscription change | The set of partitions the group should own changed. |
| Deploy / autoscale | The most common real-world cause, by a distance. |
Heartbeats and poll progress are not the same thing. A consumer can be perfectly alive and still fail to call poll() within the maximum interval because one record takes ten minutes to process. It loses its partitions while actively working. The fix is the processing architecture, not a bigger heartbeat.
Eager vs cooperative
Classic eager rebalancing revokes broad ownership before redistributing — processing pauses and lag spikes, which hurts most during frequent deploys. Sticky and cooperative approaches keep as much of the previous assignment as possible and move ownership incrementally: if C3 joins a group where C1 owns P0–P1 and C2 owns P2–P3, only P3 needs to move.
Modern Kafka also offers a newer server-driven consumer protocol (group.protocol=consumer). Treat it as a version detail after explaining the timeless idea: membership changes force ownership changes. Static membership can reduce churn from short restarts, but it is an optimisation, not a substitute for handling duplicates.
04Offsets and the failure window
read M100 ↓ external DB write succeeds ↓ CRASH ✗ offset commit never happens ↓ restart / rebalance ↓ M100 is delivered again
This is the whole reason “at-least-once” is the default mental model. The side effect and the offset commit are not one atomic transaction with an arbitrary external database, so one of them lands first and a crash between them is always possible.
| Order | Failure mode | Verdict |
|---|---|---|
| Side effect, then commit offset | Duplicate processing on crash | Usually right — pair with idempotency |
| Commit offset, then side effect | The side effect may never happen | Silent data loss; rarely what you want |
The practical fix is a unique event_id with a uniqueness constraint, treating duplicate-key as “already processed”, or a sink-specific transactional pattern where one exists.
05Hot partitions
The cluster may have enormous aggregate capacity while being completely blocked by P2's leader broker and P2's single consumer owner. If that consumer processes 20K/s and the partition receives 100K/s, lag grows at 80K/s no matter how many idle consumers are standing by.
Replication does not help either, and it is a common confusion: replicas are copies for durability and failover. There is one leader accepting writes per partition. Replication factor 3 does not mean three consumers can share the work.
06Splitting the hot key — and what it costs
The producer derives a shard from user_id, comment_id or another well-distributed attribute, producing multiple distinct Kafka keys so one hot entity can occupy many partitions.
Static vs dynamic sharding
Sharding every key costs complexity for keys that do not need it. Dynamic sharding — a control-plane record saying this entity currently uses 32 shards — avoids that, and immediately creates a distributed configuration problem instead:
- When exactly do producers switch from 1 shard to 32?
- Can events in the old and new scheme coexist in flight?
- Do consumers need a partitioning_version on every event?
- What does “ordering” even mean during the transition?
- How do you safely shrink the shard count later?
That list is why hot-key splitting is genuinely staff-level: the local fix is easy, and it exports a versioning and migration problem to everyone downstream.
Fanning one partition into a worker pool works right up until you commit offset 102 while offset 100 is still in flight — then a crash skips 100 permanently. Correct concurrent processing needs a completion tracker and a contiguous commit frontier, plus bounded queues for backpressure.
07Detecting skew
the topic average lies P0 1,000/s P1 1,000/s P2 101,000/s ← the actual problem P3 2,000/s ─────────────── average ≈ 26,000/s ← tells you nothing
| Metric, per partition | What it reveals |
|---|---|
| Messages in / sec | Skew that topic averages hide. |
| Bytes in / out | A partition can be hot on payload size at modest message counts. |
| Consumer lag | Whether one partition dominates the backlog. |
| Broker network / disk | Leader hotspot and replication pressure. |
| Produce / fetch latency | Client-visible impact. |
| Consumer processing latency | Separates a Kafka bottleneck from slow application code. |
08Question drill
8 partitions, 20 consumers in a group. How many are working?
At most 8. The other 12 own nothing for that topic and sit idle unless the group subscribes to more partitions elsewhere.
Does a Kafka message go to only one consumer group?
No. Every subscribed group consumes the record independently with its own offsets. The one-owner rule applies within a group, not across them.
Why is ordering tied to the partition key?
The key is hashed to select a partition, so all records for a key land in the same log, and a log has an order. Kafka never provides ordering across partitions — so the key is the ordering boundary.
One partition is at 100K/s. Why not add consumers?
Because that partition still has exactly one owner in the group. Extra consumers cannot share it. If the owner does 20K/s, lag grows at 80K/s with 5 consumers or 500.
Then add more partitions?
That raises aggregate parallelism but does not split one key — it still hashes to exactly one partition. It also changes key-to-partition mapping for future records, which has its own ordering consequences.
Add brokers, then?
You can move the hot partition's leader to a less loaded broker, which relocates the bottleneck without splitting it. More brokers help when load is spread across many partitions.
So what actually fixes it?
Changing the key, if the semantics allow — video_id:user_id or video_id:shard. And then being explicit that you have traded away Kafka's total order for that entity, which has to be acceptable to the product or reconstructed at read time.
Why can't Kafka just give one partition to several consumers?
Because completion order stops being knowable. If C1 has offset 100, C2 has 101 and C3 has 102, they can finish 102, 101, 100 — and the committed offset becomes meaningless as a recovery point. Ownership is what keeps ordered consumption tractable.
Consumer writes to the DB then crashes before committing. What happens?
The partition rebalances, the new owner resumes from the last committed offset, and that record is processed again. The DB write must therefore be idempotent — a unique event_id where duplicate-key means “already done”.
Why not commit the offset first to avoid duplicates?
Because then a crash means the side effect never happens at all. You have swapped duplicate processing for silent data loss, which is almost always the worse failure for a business workflow.
A record takes 10 minutes to process. What breaks?
If max.poll.interval.ms is 5 minutes, the consumer is evicted from the group while still working, its partitions move, and the work is redone elsewhere. Fix the architecture — hand long work to a separate execution system, or pause partitions while it runs — rather than only raising timeouts.
What is the group coordinator?
A broker that tracks group membership and liveness and drives the assignment protocol for that consumer group. Distinct from the cluster controller, which handles broader cluster metadata and leadership.
Eager vs cooperative rebalancing — what actually differs?
Eager revokes broad ownership and re-derives the whole assignment, pausing processing. Cooperative and sticky approaches keep most assignments and move only what must move, so a deploy does not stop the group.
How would you detect a hot partition?
Per-partition metrics, never topic averages: messages/sec, bytes in/out, and consumer lag per partition. An average of 26K/s across four partitions can hide one doing 101K/s.
09What not to say
| Do not say | Say instead |
|---|---|
| “I'll add more consumers.” | First ask whether the load is concentrated in one partition. |
| “I'll increase the partition count.” | That does not split one hot key — it still hashes to one partition. |
| “Replication factor 3 gives three consumers.” | Replicas are copies for durability and failover, not parallelism. |
| “Kafka guarantees ordering.” | Within a partition. Never across a topic. |
| “Exactly-once means no idempotency needed.” | End-to-end semantics depend on the external sink and the failure boundary. |
10Cheat sheet
| Term | One line |
|---|---|
| Partition | Append-only ordered log. The only ordering boundary Kafka gives you. |
| Offset | Partition-local position. Not comparable across partitions. |
| Consumer group | One logical application; partitions divided among its members. |
| Committed offset | The group's recovery checkpoint — not an ack of side effects. |
| Rebalance | Converging the group to a new valid ownership map. |
| Group coordinator | Broker tracking membership and driving assignment. |
| Cooperative rebalance | Move only what must move, instead of revoking everything. |
| Static membership | Stable instance identity, so short restarts churn less. |
| Hot partition | One partition taking disproportionate load while the cluster idles. |
| Key salting | Deriving several keys from one entity to spread it across partitions. |
Ordering within a partition. Never across one. Almost everything follows from that.
← all notes