How does data move between systems that never talk to each other?
They share a log. Producers append to the end of a tape, and each reader keeps its own place. Reading removes nothing, and most of what makes Kafka unusual follows from that.
One topic, one consumer group, one simulated clock. Each tick is a chance for every running consumer to process one record.
Keyed records stay in order, because one key means one partition. Turn “Use keys” off and send more: watch the sink flag records that overtake each other.
lag
lag
lag
Ready. Send records and watch them land.
Four rules explain most of it
A partition is an append-only log
Records are added at the end and get the next offset. Retention removes old records later, but consuming never does. Two groups can read the same data independently.
Order exists within a partition
There's no order across a multi-partition topic. If events about one customer must stay in sequence, give them a key so they share a partition.
One owner per partition, per group
That's what keeps a partition's order intact while it's processed. It also means more consumers than partitions adds spares, not throughput.
Progress is a number the consumer commits
The broker doesn't track each record as done. A group stores an offset per partition, and lag is the gap between that offset and the end of the log. Everything about redelivery follows from when that number gets written.
The same state, as an operator sees it
Live: this table reads the simulation above
$ kafka-consumer-groups --describe --group demo
PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG CONSUMER-ID
0 0 0 0 c1
1 0 0 0 c1
2 0 0 0 c2This is the shape of kafka-consumer-groups --describe. CURRENT-OFFSET is what the group last committed, so it moves in steps and lags behind the yellow cursor while a batch is in progress. The dashed line on each tape marks it.
A consumer is a loop around poll(): fetch a batch, process it, commit. The order of those last two steps is the choice between processing a record twice and possibly not at all.
while (running) { const batch = await consumer.poll(); for (const record of batch) { await handle(record); // crash here… } await consumer.commit(); // …and this never ran}Guarantees depend on what you built
Crash, and records come back
Experiment 4. The successor restarts from the last commit, so work between that commit and the crash runs again. Nothing is lost, and side effects may repeat.
Crash, and records vanish
Set “Commit: Before”, send 10 records, and crash a consumer while it's working. “Never processed” climbs: the group has moved past records nobody handled.
Assume delivery can repeat
Make handlers idempotent, for instance with a unique constraint on a record's ID. Turn on “Idempotent application” and duplicates arrive but do nothing.
Kafka's transactions give exactly-once processing for read-process-write flows inside Kafka. They don't extend to an arbitrary database or an email provider on the other side of your handler. That part is still yours to design.
Two predictions
A topic has six partitions. A group has eight consumers. How many can actively consume this topic at the same time?
Records A1 and B1 land in different partitions, with A1 written first. In what order will a consumer group process them?
What this model leaves out
Brokers, replication and leaders
The tapes behave as one reliable log. Real partitions are replicated across brokers with a leader and followers, and acks and min.insync.replicas decide what “written” means.
Partitioning and assignment
The default partitioner hashes keys with murmur2, and keyless records go in sticky batches, far larger than the two records used here. Assignment here is a simple range. Real strategies include cooperative-sticky, which avoids stopping every consumer during a rebalance.
Commits and timing
Commits are offsets stored in an internal topic, and clients often commit on a timer. Lag here is measured from the committed offset, as the CLI does. A running consumer's real position is usually ahead of it. Sessions are measured in seconds, not ticks.
Pausing a consumer
This models the client's pause(), which stops fetching while polling continues. A consumer that simply stops calling poll() for too long gets removed from the group, which looks more like the crash path.