Kafka Topics, Partitions, and Offsets
Watch a topic split across partitions, see how offsets are assigned per partition, and learn why Kafka only guarantees ordering inside a single partition.
A topic is not one log. It is a set of independent logs called partitions, and almost every surprising Kafka behaviour traces back to that one fact. This lesson makes the partitions visible.
Creating a topic with three partitions
bin/kafka-topics.sh --create \
--topic events \
--partitions 3 \
--replication-factor 1 \
--bootstrap-server localhost:9092
bin/kafka-topics.sh --describe --topic events --bootstrap-server localhost:9092
Created topic events.
Topic: events TopicId: mK4pQ8vXRt2yLs9nZw3BdA PartitionCount: 3 ReplicationFactor: 1 Configs:
Topic: events Partition: 0 Leader: 1 Replicas: 1 Isr: 1
Topic: events Partition: 1 Leader: 1 Replicas: 1 Isr: 1
Topic: events Partition: 2 Leader: 1 Replicas: 1 Isr: 1
Three separate logs, numbered 0, 1 and 2. Each will get its own offset sequence starting at 0.
Watching records land in different partitions
The console consumer can print the partition and offset of every record, which turns the abstract structure into something you can read. First produce six messages:
for i in 1 2 3 4 5 6; do echo "event-$i"; done | \
bin/kafka-console-producer.sh --topic events --bootstrap-server localhost:9092
Now consume with formatting turned on:
bin/kafka-console-consumer.sh \
--topic events \
--from-beginning \
--property print.partition=true \
--property print.offset=true \
--property print.value=true \
--bootstrap-server localhost:9092
Partition:0 Offset:0 event-1
Partition:0 Offset:1 event-4
Partition:1 Offset:0 event-2
Partition:1 Offset:1 event-5
Partition:2 Offset:0 event-3
Partition:2 Offset:1 event-6
Two things to notice, and both matter.
The output is not in production order. You produced event-1 through event-6 in
sequence; you got them back grouped by partition. The consumer fetches from each partition
independently and hands you whatever has arrived — there is no global sort.
Each partition has its own offset sequence. Offset 0 appears three times, once per
partition. An offset is only meaningful as the pair (partition, offset).
Within a partition, though, order is exact: partition 0 gave you event-1 then event-4,
which is the order they were written there. That guarantee never breaks.
Why the messages spread out
These records had no key. When a producer sends a keyless record, the default partitioner assigns it using a sticky round-robin: it fills one batch for a partition, then switches. With one message per batch here, the effect is plain round-robin across 0, 1, 2.
Give the records a key and the behaviour changes completely:
printf 'alice:login\nbob:login\nalice:logout\nbob:logout\nalice:purchase\n' | \
bin/kafka-console-producer.sh \
--topic events \
--property "parse.key=true" \
--property "key.separator=:" \
--bootstrap-server localhost:9092
bin/kafka-console-consumer.sh \
--topic events --from-beginning \
--property print.partition=true \
--property print.offset=true \
--property print.key=true \
--bootstrap-server localhost:9092
Partition:0 Offset:0 null event-1
Partition:0 Offset:1 null event-4
Partition:1 Offset:0 null event-2
Partition:1 Offset:1 null event-5
Partition:1 Offset:2 bob login
Partition:1 Offset:3 bob logout
Partition:2 Offset:0 null event-3
Partition:2 Offset:1 null event-6
Partition:2 Offset:2 alice login
Partition:2 Offset:3 alice logout
Partition:2 Offset:4 alice purchase
Every alice record went to partition 2. Every bob record went to partition 1. That is
not luck — the partitioner computes murmur2(key) % partition_count, so the same key
always resolves to the same partition.
And that is the mechanism behind the only ordering guarantee Kafka offers you in practice:
all records with the same key land in the same partition, so they are strictly
ordered relative to each other. Alice’s login, logout, purchase arrive in that
order, always, no matter how busy the cluster is.
Offsets are positions, not IDs
Ask for the end offset of each partition:
bin/kafka-run-class.sh kafka.tools.GetOffsetShell \
--broker-list localhost:9092 --topic events
events:0:2
events:1:4
events:2:5
Partition 2 holds five records (offsets 0-4) and will assign offset 5 next. Partition 0 holds two. The partitions are at different lengths because the key hashing sent more records to some than others — this is normal, and one reason a hot key can create a lopsided topic.
You can also start reading from a specific position rather than the beginning or end:
bin/kafka-console-consumer.sh \
--topic events --partition 2 --offset 2 \
--property print.key=true \
--bootstrap-server localhost:9092
alice login
alice logout
alice purchase
Skipping the two keyless records that occupy offsets 0 and 1 of that partition. Being able to point a consumer at an arbitrary offset is what makes reprocessing possible — you are seeking within a retained log, not re-requesting a delivery.
Practice
1. Produce ten keyless messages to events and check the end offsets. Are the partitions even?
for i in $(seq 1 10); do echo "m-$i"; done | \
bin/kafka-console-producer.sh --topic events --bootstrap-server localhost:9092
bin/kafka-run-class.sh kafka.tools.GetOffsetShell \
--broker-list localhost:9092 --topic events
events:0:12
events:1:7
events:2:8
Roughly even but not exact. The sticky partitioner batches records per partition before switching, so short bursts can land unevenly. Over a large volume it evens out.
2. Send three records keyed order-42. Which partition do they land in, and why can you predict it?
printf 'order-42:created\norder-42:paid\norder-42:shipped\n' | \
bin/kafka-console-producer.sh --topic events \
--property parse.key=true --property key.separator=: \
--bootstrap-server localhost:9092
Partition:0 Offset:12 order-42 created
Partition:0 Offset:13 order-42 paid
Partition:0 Offset:14 order-42 shipped
All three in one partition, in order. The partition is murmur2("order-42") % 3, which is
deterministic — the same key on the same partition count always resolves the same way.
3. Increase events to 5 partitions, then produce order-42 again. Did it stay in the same partition?
bin/kafka-topics.sh --alter --topic events --partitions 5 \
--bootstrap-server localhost:9092
printf 'order-42:delivered\n' | bin/kafka-console-producer.sh --topic events \
--property parse.key=true --property key.separator=: \
--bootstrap-server localhost:9092
Partition:3 Offset:0 order-42 delivered
It moved. The partitioner divides by the partition count, so changing that count remaps
every key. order-42’s history is now split across two partitions with no ordering
between them — which is exactly why adding partitions to a keyed topic is a breaking
change, not a routine scale-up.
4. Read only partition 1, starting at offset 2, and stop after 2 messages.
bin/kafka-console-consumer.sh --topic events \
--partition 1 --offset 2 --max-messages 2 \
--property print.offset=true \
--bootstrap-server localhost:9092
Offset:2 login
Offset:3 logout
Processed a total of 2 messages
--max-messages makes the consumer exit instead of waiting, which is useful in scripts.
Next: producing from application code rather than the console, and what the producer actually does with your record before it reaches the broker.