# 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.

- stage: growing
- published: 2026-04-22
- updated: 2026-06-28
- tags: kafka, distributed-systems, go

---
> 🧪 **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.