Kafka in Production · Kafka Patterns and Operations
Keys, Partitioning and Hot Partitions
Choose message keys for ordering and balance, spot hot partitions, and understand why adding partitions breaks key-to-partition mapping.
What you'll build
An experiment with key choice: keying by tenant puts one huge tenant's traffic on one partition (a hot partition), keying by order ID spreads load but loses per-tenant ordering, and adding partitions to a topic moves keys to different partitions.
The idea
- The default partitioner hashes the key: the same key always goes to the same partition, as long as the partition count doesn't change.
- Choose keys by what must stay ordered (an account, an order), with enough distinct values to spread load.
- Skewed keys create hot partitions: one consumer does most of the work while others idle.
- Adding partitions remaps keys, breaking ordering for keys during the change; plan partition counts up front (or create a new topic and migrate).
The code
import os
from collections import Counter
from confluent_kafka import Producer
from confluent_kafka.admin import AdminClient, NewPartitions, NewTopic
BOOTSTRAP = os.environ["KAFKA_BOOTSTRAP"]
admin = AdminClient({"bootstrap.servers": BOOTSTRAP})
admin.create_topics([NewTopic("events", num_partitions=4, replication_factor=1)])["events"].result()
producer = Producer({"bootstrap.servers": BOOTSTRAP})
def send_all(keys):
placed = []
for key in keys:
producer.produce("events", key=key, value=b"x", on_delivery=lambda err, msg: placed.append((msg.key().decode(), msg.partition())))
producer.flush()
return placed
tenants = ["bigcorp"] * 80 + ["shop-a"] * 10 + ["shop-b"] * 10
by_tenant = Counter(p for _, p in send_all(tenants))
print("keyed by tenant, messages per partition:", [by_tenant.get(p, 0) for p in range(4)])
by_order = Counter(p for _, p in send_all([f"order-{i}" for i in range(100)]))
print("keyed by order id, messages per partition:", [by_order.get(p, 0) for p in range(4)])
keys = [f"order-{i}" for i in range(20)]
before = dict(send_all(keys))
admin.create_partitions([NewPartitions("events", 8)])["events"].result()
producer = Producer({"bootstrap.servers": BOOTSTRAP}) # a fresh producer sees the new partition count
after = dict(send_all(keys))
moved = sum(before[k] != after[k] for k in keys)
print(f"after growing 4 -> 8 partitions, {moved} of {len(keys)} keys map to a different partition")
admin.delete_topics(["events"])["events"].result()
Output
keyed by tenant, messages per partition: [10, 80, 10, 0] keyed by order id, messages per partition: [24, 26, 24, 26] after growing 4 -> 8 partitions, 10 of 20 keys map to a different partition
Run this on your computer — see “Get Started” for setup.
Common mistakes
- Keying by a low-cardinality or skewed value (country, tenant) and wondering why one consumer is overloaded.
- No key at all for events that must stay ordered per entity.
- Increasing partitions on a topic whose consumers rely on key ordering, without a migration plan.
Key points
- The key decides the partition and the ordering scope.
- Pick keys with enough cardinality; watch for skew.
- Adding partitions remaps keys; size topics ahead or migrate deliberately.
Further reading: Producer configs: partitioner · Choosing the number of partitions
Practice questions
What is a hot partition?
Answer
A partition that receives a disproportionate share of messages because of a skewed key, overloading the consumer that owns it.
Why is adding partitions risky for keyed topics?
Answer
The key-to-partition mapping changes, so new messages for a key may land in a different partition than older ones, breaking per-key ordering during the transition.
How do you choose a message key?
Answer
Use the entity whose events must stay ordered (account, order, device), with enough distinct values to spread load evenly.
Open in the app to mark lessons complete, take notes and track your progress.