Skip to content
dethlex

Kafka partition assignment: Range, RoundRobin, and why joins care

RangeAssignor looks naive until you join two topics — then its alignment is the whole point. Choosing an assignment strategy is choosing what your consumers are allowed to assume.

growingplanted tended 2 min read

🧪 Sample seed note. Placeholder content demonstrating the garden structure — replace with your own text.

A consumer group is a little scheduler: topics on one side, consumers on the other, and an assignor deciding who reads what. Most people meet assignors the day load lands unevenly on their pods. The more interesting encounter is the day a stream-stream join silently produces garbage.

RangeAssignor: skewed, but aligned

RangeAssignor works per topic: sort the partitions, sort the consumers, carve the partitions into contiguous ranges. With 8 partitions and 3 consumers you get 3-3-2 — the first consumers always take the extra partitions, for every topic they subscribe to. Subscribe one group to five 8-partition topics and consumer 0 carries five extra partitions while consumer 2 carries none. That is the famous Range skew.

But look at what the per-topic sorting buys: partition N of every subscribed topic lands on the same consumer. If impressions and clicks are both keyed by campaign_id, both have 8 partitions, and both were written with the same partitioner, then all events for one campaign — from both topics — arrive at one process. You can join them in local memory with no shuffle, no external store. This property is called co-partitioning, and Range-style assignment is the only reason it survives group rebalancing.

RoundRobin and Sticky: even, but blind

RoundRobinAssignor throws every topic-partition into one pool and deals them out like cards. Load: even. Alignment: destroyed — impressions-3 and clicks-3 now live on different consumers, and your in-memory join is quietly wrong.

StickyAssignor and its successor CooperativeStickyAssignor optimize a different axis: keep the previous assignment as intact as possible across rebalances. Cooperative rebalancing additionally kills the stop-the-world pause — consumers only release the partitions that actually move. For a plain fan-out consumer group this is what you want in 2026.

The actual decision table

You are… Pick
joining topics on a shared key Range (and see checklist)
running a plain high-volume fan-out CooperativeSticky
on Kafka Streams nothing — it brings its own

The co-partitioning checklist, all four or nothing:

  1. Same partition count on every joined topic.
  2. Same key and serialization.
  3. Same partitioner on every producer.
  4. A Range-family assignor on the consumer group.

Break any one of them — someone “rebalances” partition counts, one producer uses murmur2 and another crc32 — and the join degrades without a single error in any log. The only alarm you’ll get is a business metric drifting.

In Go this is explicit: franz-go takes kgo.Balancers(kgo.CooperativeStickyBalancer()) (or kgo.RangeBalancer()), segmentio/kafka-go takes GroupBalancers. Choose consciously — the default depends on the library, and as shown above, the default is a semantic decision, not a tuning knob.