Module P-15·36 min read

The log as source of truth, partitions and consumer groups, offset management, and the rebalance storm that stops all consumption at once.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

Module P-15 — Event Streaming with Kafka

What this module covers: Kafka is Module F-2's central finding sold as a product: an append-only log, written sequentially, served out of the page cache. This module covers the log as a primary abstraction and how it differs from the destructive queues of Module P-14, why sequential append plus zero-copy makes a broker fast, partitions as the unit of both parallelism and ordering, keys and the per-partition-only ordering guarantee, consumer groups and offset commits and the at-most-once/at-least-once fork they create, rebalance storms and their three mitigations, consumer lag, retention and log compaction, why partition count is nearly irreversible, and the operational bill you pick up by adopting it.


Kafka Is Module F-2 Turned Into a Product

Module F-2 ended on a number that surprises most people: a sequential write to an NVMe SSD sustains 3–7 GB/s, while a random access pattern against DRAM — nominally 20–50 GB/s — delivers something closer to 80 MB/s of useful bytes, because every access pulls a 64-byte cache line to use eight bytes of it and defeats the prefetcher. Sequential beats random by so much that the storage medium stops being the interesting variable. The access pattern is.

Kafka is what happens when someone takes that seriously and designs an entire product around one access pattern. There is no B-tree. There is no in-place update. There is no random write path to optimise, because there is no random write. A Kafka topic partition is a file that only ever grows at the end, and a Kafka consumer is a cursor that only ever moves forward through it. Everything else — the replication, the consumer groups, the retention policies — is scaffolding around that single decision.

That is why a Kafka broker sustains hundreds of MB/s of sequential throughput on ordinary hardware while a Postgres instance with fsync on commit tops out at roughly 1K–5K transactions per second (Module F-3's capacity anchors). It is not that Kafka is better engineered than Postgres. It is that Postgres has to support random reads, in-place updates, secondary indexes, and MVCC visibility rules, and Kafka has decided in advance that you do not get any of those. Module F-14's rule — NoSQL scales by removing features — applies here in its purest form. Kafka scales by removing almost all of them.

The primary abstraction is the log. Not a queue, not a topic, not a message. A log: an ordered, append-only, immutable sequence of records, each with a monotonically increasing position called an offset. Records are retained after they are read. Reading does not consume. Ten independent consumers can read the same partition at the same time, at different offsets, and none of them affects the others, because reading is just a file read at a byte position.

Module P-14's queues work the other way. A message is delivered, acknowledged, and deleted — consumption is destructive, and the broker's job is to track the state of every individual message (invisible, in-flight, acked, dead-lettered). That per-message state is what makes a queue broker's storage random-access, and it is also what makes a queue impossible to replay.

Queue (Module P-14)Log (Kafka)
ConsumptionDestructive — ack deletes the messageNon-destructive — offset advances, record stays
Broker state per messageVisibility, in-flight, retry count, DLQNone; only the record's byte position
Multiple independent readersNeeds fan-out to N queuesNative — each group has its own offsets
ReplayImpossible once ackedReset the offset and read again
OrderingBest-effort, breaks under redeliveryStrict within a partition
Storage access patternRandom-ishPurely sequential
Per-message retry/delayBuilt inYou build it (Module P-19)
Natural unit of scaleConsumersPartitions

Read that last-but-one row carefully, because it is the honest cost of the log model and it is the one teams discover late. A queue gives you per-message retry, per-message delay, and a dead-letter queue as first-class features. Kafka gives you none of them, because a log has no per-message state to hang them off. If record 4,001 fails and you want to skip it and continue, you have to build that yourself — commit past it and write it to a separate retry topic. Kafka's ordering guarantee and its lack of per-message handling are the same property viewed from two sides.

Analogy: a queue is the spike on a diner counter — the cook takes the docket off, fills the order, and bins it; the docket is gone, and only one cook can ever hold it. Kafka is the till roll. The printer only advances, nothing is ever erased, and any number of people can walk up and read from any line number they like — the kitchen reading line 400, the accountant reading line 12, the auditor starting again from line 1 next March. The roll is the record; where each reader is up to is just a line number they keep in their own pocket.


Why It's Fast: The Page Cache Does the Work

Three mechanisms, all of them Module F-2 in application.

Sequential append. A produce request appends to the active segment file of a partition. No seek, no index maintenance, no page split, no read-modify-write. On spinning disks — where Kafka's design was born — this was the difference between ~150–250 MB/s sequential and 100–200 IOPS random. On NVMe the ratio narrows but the direction holds.

The page cache, not a heap cache. Kafka does not maintain its own record cache. It writes to the OS page cache and lets the kernel flush; it reads from the page cache too. So a consumer keeping up with the tail of the log is reading pages that were written seconds ago and are still resident in memory — the read never touches disk at all. This is why Kafka brokers are given modest JVM heaps and enormous amounts of RAM: the RAM is for the kernel, not for Kafka. It also means the working-set arithmetic from Module F-12 applies directly. Consumers reading the tail are a memory workload. A consumer that has fallen an hour behind is a disk workload, and it evicts pages that the caught-up consumers wanted.

Zero-copy on the read path. Because a stored record is byte-for-byte what goes out on the wire, the broker can hand a file region straight to a socket via sendfile — the data goes page cache → NIC without being copied into the JVM and back out. That eliminates two copies and a great deal of garbage collection pressure.

Why this matters in production: the zero-copy path is conditional, and the conditions are easy to break. Enable TLS and the broker must encrypt in user space, so the copies come back. Let producers and consumers disagree on message format or compression such that the broker has to re-compress or convert, and the copies come back along with substantial CPU cost. A cluster that was network-bound becomes CPU-bound, and the symptom is rising produce latency with disk and network both apparently idle. The fix is usually to make producers, brokers, and consumers agree on compression codec and format so the broker can pass bytes through untouched — but TLS is not optional in most environments, and the honest position is that you pay for it. Budget CPU accordingly rather than being surprised by it.


Partitions Are Both Your Parallelism and Your Only Ordering Guarantee

A topic is a named stream. It is a logical thing; it does not exist on disk. A topic is divided into partitions, and a partition is the real object: one directory of segment files on one broker (plus its replicas), with its own offset sequence starting at 0.

Everything important about Kafka's data model follows from partitions being the physical unit:

  • Parallelism is capped by partition count. Within a consumer group, a partition is assigned to at most one consumer. Twelve partitions means at most twelve useful consumer instances; the thirteenth sits idle holding nothing. Adding consumers beyond the partition count does not add throughput, which is the single most common wasted deployment in Kafka operations.
  • Ordering exists only inside a partition. Kafka guarantees that records in a partition are read in the order they were written. It guarantees nothing whatsoever about the relative order of records in different partitions of the same topic — they are separate files, read by separate consumers, at separate speeds.
  • Throughput and durability are per-partition. Each partition has one leader replica handling all reads and writes for it, with followers replicating. A partition is therefore also the unit of load imbalance.

The bridge between "I need ordering" and "ordering is per-partition" is the key. The producer computes the partition from a hash of the record key:

text

So key = order_id puts every event about one order into one partition, in order, forever. That is the guarantee you actually get, and it is worth stating in the exact words you should use in a design review: Kafka gives you same-key ordering, never global ordering.

typescript

The cost of keying is a hot partition, and it is the same cost as Module F-14's partition-key problem wearing different clothes. Key by tenant_id in a system where one tenant is 40% of traffic and one partition is 40% of the load — on one broker, consumed by one consumer, which now sets the lag for the whole topic while eleven others idle. Module P-8's isolation problem, arriving through the message bus. The mitigations are the familiar ones: a composite key (tenant_id:order_id) if you only need ordering at the finer grain, or an explicit custom partitioner that splits the whale across several partitions and accepts that its events are no longer ordered relative to each other.

The other cost of keying is that it makes partition count nearly immutable, which is its own section below.

A guarantee people assume and do not have: ordering across topics. Two topics are unrelated logs. If your service writes payment-events and order-events and a consumer joins them, there is no ordering relationship between them at all — not even loosely. Systems that need cross-entity ordering need a single topic with a shared key, or a design that does not depend on the ordering (Module P-16's approach: make handlers order-insensitive rather than trying to enforce order).


Consumer Groups, Offsets, and Where "At-Least-Once" Comes From

A consumer group is a set of consumer instances sharing a group.id. The group's coordinator (a broker) divides the topic's partitions among the live members, and stores the group's committed offsets in an internal topic, __consumer_offsets. Two different groups reading the same topic are completely independent — separate offsets, separate lag, separate progress. That independence is the whole reason a log supports fan-out without duplicating data: your search indexer, your analytics loader, and your email service are three groups on one topic, not three copies of a queue.

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.