Kafka partitions, keys and ordering

Queues, Kafka and event-driven systems Mid-level Python 3.13+ PostgreSQL 17+ Reviewed By Best Latest Apps

Pick the fix

Events for one order must be processed in sequence. How do you produce them?

Options
Hint 1 of 1

Order holds within what?

Choose an answer, then open Show answer below to check it.

Three partitions, each a row of offsets with message keys. Two consumers share them, a third joins and each owns one, a fourth joins and sits idle, then C2 leaves and its partition moves to C4.

Run on Kafka 4

Text version

P0: offsets 0 A, 1 A, 2 C, 3 A, 4 B; P1: offsets 0 B, 1 D, 2 D, 3 B; P2: offsets 0 C, 1 E, 2 E, 3 F, 4 C.

Start: C1 and C2. C1: P0, P1; C2: P2.

  1. C3 joins (consumer C3 subscribes): C1: P0; C2: P1; C3: P2. Rebalance: one partition each.
  2. C4 joins (consumer C4 subscribes): C1: P0; C2: P1; C3: P2; C4: idle. C4 is idle: 3 partitions, 4 consumers.
  3. C2 leaves (C2 closes): C1: P0; C3: P2; C4: P1. P1 moves to C4, which resumes at the committed offset.
Show answer

Answer: A Use the order id as the message key

Correct. All of that order's events go to one partition, which keeps order.

Kafka only guarantees order within a partition, and the key's hash picks the partition. Keying by order id sends all of one order's events to the same partition, in order.

Why not the others?
  • B Use a random key. Spreads one order's events across partitions.
  • C Use one partition for the whole topic. Orders are kept, but parallelism is gone.
  • D Add a timestamp and sort in the consumer. Consumers on different partitions still race.

30-second answer

A topic is split into partitions, each an append-only log. With a key, the producer hashes it to choose a partition, so every message for one key lands in the same partition; without a key, messages are spread for balance. Kafka guarantees order only within a partition, so key by the entity whose events must stay in order, such as the order id. In our Kafka 4 run, every key went to the same partition each time.

Practise saying it · 35 s. No recording, just a timer

Explain Kafka partitioning.

Say your answer out loud, then check which of these points you covered:

  • Partition = ordered log
  • Key hash picks partition
  • Order per partition only
  • Partitions cap parallelism
  • Repartitioning breaks key order

Deep dive

Adding partitions later changes the key-to-partition mapping for new messages and breaks per-key ordering across the change.

The Java client hashes keys with murmur2; librdkafka (Python's confluent-kafka) defaults to a CRC32-based partitioner, so mixed clients can disagree unless configured.

Partitions cap consumer parallelism: a group can't have more active consumers than partitions.

A hot key makes one partition hot; you can't spread it without giving up its ordering.

Keys and consumer groups on a real broker Ran on Python 3.13.5 · mypy --strict
Python 3.13
import threading
import time
import uuid

from confluent_kafka import (
    Consumer,
    KafkaError,
    Message,
    Producer,
    TopicPartition,
)
from confluent_kafka.admin import AdminClient
from confluent_kafka.cimpl import NewTopic

Conf = dict[str, str | int | float | bool]
BOOT: Conf = {"bootstrap.servers": "127.0.0.1:9092"}
topic = f"orders-{uuid.uuid4().hex[:8]}"
admin = AdminClient(BOOT)
admin.create_topics([NewTopic(topic, num_partitions=3)])[topic].result()

# 1. The same key always lands on the same partition.
seen: dict[str, set[int]] = {}


def report(err: KafkaError | None, msg: Message) -> None:
    key = str(msg.key() or b"", "utf-8")
    seen.setdefault(key, set()).add(msg.partition() or 0)


p = Producer(BOOT)
for _ in range(2):
    for user in ("user-1", "user-2", "user-3"):
        p.produce(topic, key=user, value=b"{}", on_delivery=report)
p.flush(10)
sticky = all(len(v) == 1 for v in seen.values())
print("Same key, same partition:", sticky)

# 2. Four consumers, three partitions: one consumer gets nothing.
owned: dict[str, list[int]] = {}


def member(name: str, stop: threading.Event) -> None:
    def on_assign(c: Consumer, parts: list[TopicPartition]) -> None:
        owned[name] = sorted(tp.partition for tp in parts)

    conf: Conf = {**BOOT, "group.id": topic}
    c = Consumer(conf)
    c.subscribe([topic], on_assign=on_assign)
    while not stop.is_set():
        c.poll(0.2)
    c.close()


stop = threading.Event()
names = ["C1", "C2", "C3", "C4"]
ts = [threading.Thread(target=member, args=(n, stop)) for n in names]
for t in ts:
    t.start()
deadline = time.time() + 40
while time.time() < deadline:
    if len(owned) == 4 and sum(map(len, owned.values())) == 3:
        break
    time.sleep(0.5)
stop.set()
for t in ts:
    t.join()
counts = sorted(len(v) for v in owned.values())
print("Partitions per consumer:", counts)
print("Idle consumers:", counts.count(0))

Common mistakes

  • Expecting order across a whole topic.
  • Producing without keys and expecting per-entity order.
  • Doubling partitions on a keyed topic without planning.

Senior take & follow-ups

Partition = log, key hashing, per-partition order, and the cost of changing partition counts.

Follow-up questions

Think of your answer first, then open the hint.

  1. How many partitions should a topic have?

    Show hint

    Enough for peak consumer parallelism with headroom; more isn't free.

  2. Why might two clients pick different partitions?

    Show hint

    Different default partitioners.

Suggest a fix if something here is wrong or out of date.