Skip to main content
Kafka intermediate Lesson 5 of 10

Keys, Partitioning, and Ordering Guarantees

See how a record key decides its partition, why that is the only ordering guarantee Kafka gives you, and what a custom partitioner changes.

The default partitioner is nine lines of logic that decide almost everything about how a Kafka topic behaves under load. This lesson makes that logic explicit.

The rule

For a record with a key, the partition is:

partition = murmur2(serialized_key) % number_of_partitions

For a record without a key, the producer uses a sticky assignment — it picks one partition, fills a batch, then moves to another. Neither case involves the broker; the producer decides the partition before the record leaves the process.

You can compute it yourself and predict where a record will land:

def murmur2(data: bytes) -> int:
    """Kafka's partitioner hash — a port of the Java client's implementation."""
    length = len(data)
    seed = 0x9747B28C
    m, r = 0x5BD1E995, 24
    h = (seed ^ length) & 0xFFFFFFFF

    for i in range(0, length - length % 4, 4):
        k = (data[i] | data[i + 1] << 8 | data[i + 2] << 16 | data[i + 3] << 24)
        k = (k * m) & 0xFFFFFFFF
        k ^= k >> r
        k = (k * m) & 0xFFFFFFFF
        h = ((h * m) & 0xFFFFFFFF) ^ k

    rem = length % 4
    if rem >= 3: h ^= data[length - rem + 2] << 16
    if rem >= 2: h ^= data[length - rem + 1] << 8
    if rem >= 1:
        h ^= data[length - rem]
        h = (h * m) & 0xFFFFFFFF

    h ^= h >> 13
    h = (h * m) & 0xFFFFFFFF
    h ^= h >> 15
    return h

def partition_for(key: str, partitions: int) -> int:
    # The Java client masks off the sign bit before taking the modulo.
    return (murmur2(key.encode()) & 0x7FFFFFFF) % partitions

for key in ["alice", "bob", "carol", "dave", "order-42"]:
    print(f"{key:10} -> partition {partition_for(key, 3)}")
$ python partitioner.py
alice      -> partition 2
bob        -> partition 1
carol      -> partition 2
dave       -> partition 0
order-42   -> partition 0

Produce those keys and check — the assignment matches exactly. alice and carol share partition 2, which is normal: with three partitions and five keys, collisions are guaranteed.

Why this is the ordering guarantee

Kafka’s ordering promise is narrow and worth stating precisely: records within one partition are delivered in the order they were written, and nothing else is ordered.

Because a key always maps to one partition, all records sharing a key are in one partition, so they inherit that ordering. This is the whole mechanism. Keying by order_id means an order’s created → paid → shipped sequence can never be observed out of order, even though the topic as a whole has no order at all.

Watch it fail without a key:

from confluent_kafka import Producer

producer = Producer({"bootstrap.servers": "localhost:9092"})
for state in ["created", "paid", "shipped", "delivered"]:
    producer.produce("lifecycle", value=state)   # no key
producer.flush()
bin/kafka-console-consumer.sh --topic lifecycle --from-beginning \
  --property print.partition=true --max-messages 4 \
  --bootstrap-server localhost:9092
Partition:1	paid
Partition:2	shipped
Partition:0	created
Partition:0	delivered
Processed a total of 4 messages

paid arrived before created. A consumer reading this stream would process a payment for an order it has never seen. Nothing is broken — you simply never asked for ordering.

Add the key and re-run:

for state in ["created", "paid", "shipped", "delivered"]:
    producer.produce("lifecycle", key="order-42", value=state)
Partition:0	created
Partition:0	paid
Partition:0	shipped
Partition:0	delivered
Processed a total of 4 messages

One partition, correct order, every time.

The hot-key problem

Keying buys ordering and costs balance. If one key dominates your traffic, its partition does all the work while the others idle:

from confluent_kafka import Producer
from collections import Counter

producer = Producer({"bootstrap.servers": "localhost:9092"})
landed = Counter()

def track(err, msg):
    if not err:
        landed[msg.partition()] += 1

# 90% of traffic from one tenant — a realistic multi-tenant skew
keys = ["tenant-mega"] * 900 + [f"tenant-{i}" for i in range(100)]
for k in keys:
    producer.produce("usage", key=k, value="event", callback=track)
producer.poll(0)
producer.flush()

for p in sorted(landed):
    print(f"partition {p}: {landed[p]:4} records")
$ python hot_key.py
partition 0:   38 records
partition 1:  931 records
partition 2:   31 records

Partition 1 holds 93% of the data. The consumer assigned to it becomes the bottleneck, and adding consumers does not help — no other consumer is allowed to read that partition. This is the standard reason a Kafka pipeline stops scaling.

The fix is a compound key that preserves the ordering you actually need while spreading load. If you only need ordering per (tenant, user) rather than per tenant:

for k in keys:
    user = f"u{hash(k) % 50}"
    producer.produce("usage", key=f"{k}:{user}", value="event", callback=track)
partition 0:  334 records
partition 1:  341 records
partition 2:  325 records

Even distribution, and ordering is still exact within each tenant-user pair.

Custom partitioners

When hashing is not the right rule — routing by region, isolating a tenant onto dedicated partitions — you can supply the partition explicitly:

REGION_PARTITION = {"eu": 0, "us": 1, "apac": 2}

for region, payload in [("eu", "e1"), ("us", "u1"), ("apac", "a1"), ("eu", "e2")]:
    producer.produce(
        "regional",
        key=region,
        value=payload,
        partition=REGION_PARTITION[region],   # bypasses the partitioner entirely
        callback=track,
    )
producer.flush()
Partition:0	eu	e1
Partition:0	eu	e2
Partition:1	us	u1
Partition:2	apac	a1

Explicit partitions are a commitment: the mapping is now in your application code, and changing the partition count means changing that code too. Use it when the routing rule is genuinely a business rule, not to avoid understanding the hash.

Practice

1. Predict which partition user-7 lands in on a 4-partition topic, then verify.
print(partition_for("user-7", 4))
1
printf 'user-7:hello\n' | bin/kafka-console-producer.sh --topic p4 \
  --property parse.key=true --property key.separator=: \
  --bootstrap-server localhost:9092
Partition:1	user-7	hello

The producer runs the same hash you just ran, so the prediction is exact — not probabilistic.

2. Compute the partition for the same key at 3, 4, and 5 partitions. What does this tell you about resizing?
for n in (3, 4, 5):
    print(f"{n} partitions -> {partition_for('order-42', n)}")
3 partitions -> 0
4 partitions -> 3
5 partitions -> 3

The key moves as the count changes. Any partition-count change scrambles key placement, so a keyed topic cannot be resized without breaking per-key ordering for records already written. Plan partition count for peak load up front, or accept a migration to a new topic.

3. Produce 1000 keyless records to a 3-partition topic. How even is the spread?
partition 0:  336 records
partition 1:  332 records
partition 2:  332 records

Near-perfect, because the sticky partitioner rotates through partitions once batches fill. Keyless records are the right default whenever you do not need ordering — they scale without a hot-key risk.

4. A topic has 3 partitions and one key carries 80% of traffic. You add 3 more partitions. Does the hot partition get cooler?

No. The hot key still hashes to exactly one partition — it just may be a different one. Partition count affects distribution across keys, never within a key. The only fixes are a compound key that splits the entity, or explicit routing. This is the most common misdiagnosis in Kafka capacity work: adding partitions to solve a skew that partitions cannot solve.

Next: what “delivered” actually means, and the three delivery guarantees you can choose between.

Frequently Asked Questions

What happens to ordering if I add partitions to a keyed topic?
It breaks for existing keys. The default partitioner computes murmur2(key) % partition_count, so changing the count remaps keys to different partitions. A key's old records stay in the old partition while new ones go elsewhere, and there is no ordering between two partitions.
Can two different keys land in the same partition?
Yes, and it is expected. With 1000 keys and 3 partitions roughly 333 keys share each partition. The guarantee is that one key never spreads across partitions, not that a partition holds only one key.
Should every record have a key?
Only if you need per-entity ordering or log compaction. A key forces all records for that entity onto one partition, which is what gives you ordering — but it also means a hot entity can overload a single partition. Keyless records spread evenly and scale better.
Does Kafka guarantee exactly-once ordering across a topic?
No. Kafka offers no total order across partitions at any setting. The only way to get a global order is a single-partition topic, which caps throughput at one broker's capacity for that log.