Inside a Kafka Log Segment: Every Byte on Disk, Explained
A deep, step-by-step tour of how Kafka stores your messages in segment files, how record batches, indexes, and checkpoints fit together, and why this simple design makes Kafka fast, durable, and easy to operate.
Imagine a huge library that never throws away a page as it is being written. New pages are only ever added at the very end of the newest notebook. When a notebook gets full, the librarian closes it, puts a label on its spine showing the number of its first page, and starts a fresh notebook. Old notebooks are never edited. When they are too old, the whole notebook is taken off the shelf at once. Apache Kafka stores your data in almost exactly this way, and each of those notebooks is called a log segment.
Most people use Kafka for years without ever opening a segment file. That is fine for everyday work. But when a disk fills up unexpectedly, when retention seems to be ignored, when a broker takes an hour to restart, or when a consumer seems slow for no reason, the answer almost always lives inside the segment files. This tutorial opens them up. We will start with the folder layout, move to the exact structure of the data file, study the helper files that make reads quick, and finish with the operational lessons that matter in production.
1What a Partition Really Looks Like on Disk
Before we look inside a segment, we need to know where segments live and how they are named.
From topic to folder
A Kafka topic is a named stream of messages, such as orders. A topic is split into partitions, and each partition is an independent, ordered list of messages. On a broker, every partition that the broker hosts becomes one plain folder on the disk. The folder name is the topic name, a dash, and the partition number. So partition 0 of the orders topic becomes a folder called orders-0.
All of these folders sit inside the directories you list in the broker setting log.dirs. Despite the confusing name, this setting has nothing to do with application logs like error messages. It is the place where Kafka keeps its data. Some teams list several directories, one per physical disk, so the broker can spread partitions across disks.
Think of the log.dirs directory as a big filing cabinet. Each partition is one drawer, labeled with the topic name and a number. Inside each drawer sits a stack of notebooks. The drawer never holds anything else, so if you want to know everything about one partition, you only open one drawer.
graph TD
A["log.dirs directory on the broker"] --> B["orders-0 folder"]
A --> C["orders-1 folder"]
A --> D["payments-0 folder"]
B --> E["Segment files: .log, .index, .timeindex"]
B --> F["Checkpoint and state files"]
C --> G["Segment files"]
D --> H["Segment files"]
Segment names are numbers with a meaning
Inside a partition folder you will see files whose names are long strings of digits, such as 00000000000000000000.log or 00000000000000368769.log. That number is the base offset of the segment. An offset is the position number of a message inside a partition. The first message ever written gets offset 0, the next gets 1, and so on. The base offset is simply the offset of the first message that lives in that segment.
The name is padded with zeros to a fixed length of twenty digits. This is a clever small trick. Because every name has the same length, sorting file names alphabetically also sorts them by offset. The largest possible offset in Kafka fits in nineteen digits, so twenty digits is always enough. A person listing the folder sees the history of the partition in perfect order without any special tool.
The base offset in the file name is the only place that tells Kafka where a segment begins. Never rename segment files by hand. If the name and the content disagree, the broker cannot find messages correctly.
2Why Kafka Splits a Partition into Segments
If one huge file could hold a whole partition, why does Kafka bother with many smaller ones?
The idea of a commit log is very old. You only append to the end, and you never change what was already written. Kafka builds on this idea, but it does not keep one endless file. It cuts the log into pieces, and there are four strong reasons for that.
Cheap deletion
Removing old data means deleting a whole file. That is one fast operation, instead of rewriting a giant file to cut out its head.
Small, fast indexes
Each segment has its own index. A short index is quick to search and can be held in memory without pressure.
Safe, simple writing
Only the newest segment is ever written. All older segments are frozen, so they can be read, copied, or compacted without fighting with writers.
Small numbers inside
Indexes store positions as small numbers relative to the segment start. Smaller numbers mean smaller indexes and less memory.
A newspaper company does not print one endless scroll. It prints one paper per day. When the archive gets too big, the company throws away all papers older than a year, one bundle at a time. Nobody needs to cut old stories out of the middle of a scroll.
The two kinds of segments
At any moment a partition has exactly one active segment. This is the newest one, and it is the only one that accepts new messages. Every other segment is closed, also called rolled or inactive. Closed segments never change their message content. This split matters a lot because many Kafka rules are stated in terms of it. For example, the retention system may only delete closed segments, and the log cleaner only works on closed segments.
Many beginners expect Kafka to delete a message the exact second it becomes older than the retention time. It does not. Kafka deletes whole closed segments, so a message can outlive its retention time until its entire segment is eligible. We come back to this in the retention chapter.
3The Family of Files Inside a Partition Folder
A segment is not one file. It is a small family of files that share the same base offset in their names.
When people say “segment file”, they usually mean the .log file. But the broker creates several companions for it. Each companion has one job. Here is the complete family, from most important to least common.
| File | What it holds | Always present? |
|---|---|---|
.log | The actual messages, stored as record batches | Yes |
.index | A sparse map from offset to byte position in the .log file | Yes |
.timeindex | A sparse map from timestamp to offset | Yes |
.txnindex | A list of aborted transactions found in this segment | Only when aborted transactions exist |
.snapshot | A saved picture of producer state, used for duplicate detection and transactions | Created as segments roll |
.cleaned, .swap, .deleted | Temporary states used during compaction and deletion | Only for a short time |
Files that belong to the whole partition
Besides the per-segment files, the partition folder holds a few files that describe the partition as a whole. The most important is leader-epoch-checkpoint, a small text file that records which broker was leader during which range of offsets. You will also find partition.metadata, which stores the unique ID of the topic. And at the top level of each log.dirs directory there are checkpoint files such as recovery-point-offset-checkpoint, replication-offset-checkpoint, log-start-offset-checkpoint, and cleaner-offset-checkpoint, plus a meta.properties file that identifies the broker and the cluster.
The metadata log uses the same design
In clusters that run KRaft mode, Kafka’s own cluster metadata is also stored as a log made of segments. The same append-only idea that protects your business data protects the cluster’s own brain, which shows how general and trusted this format is.
4The .log File: A Plain Stream of Record Batches
The data file has almost no structure of its own. It is just batches placed one after another.
If you could look at the raw bytes of a .log file, you would not find a file header, a footer, a table of contents, or a special end marker. The very first byte of the file is already the start of the first record batch. When that batch ends, the next batch begins immediately, and so on until the file ends. This “nothing but data” design is one reason writes are so fast: the broker only ever pushes bytes to the end.
What is a record batch?
A record batch is a group of one or more messages that were packed together by the producer. Kafka calls each individual message a record. A producer does not send every record by itself. It waits a little, collects records for the same partition, and sends them as one batch. The controls for this are batch.size and linger.ms on the producer side.
The batch is the unit of everything important in Kafka: it is the unit of network transfer, the unit of compression, the unit of checksum checking, and the unit that is stored on disk. The broker does not usually pull a batch apart. It stores the very same bytes that arrived over the network.
Think of a postal parcel. Inside are many letters (records). The post office does not open the parcel to sort each letter. It looks at the label on the outside (the batch header), checks it, and moves the whole parcel along. Kafka treats batches like sealed parcels.
graph LR
A["Batch 1: offsets 0 to 4"] --> B["Batch 2: offsets 5 to 9"]
B --> C["Batch 3: offsets 10 to 10"]
C --> D["Batch 4: offsets 11 to 25"]
D --> E["Next batch goes here"]
Offsets belong to records, batches carry the range
Every record has its own offset, but a batch does not store a separate offset for each record in full. It stores one base offset for the first record and a small offset delta for each record. To find the real offset of a record, you add its delta to the base offset of the batch. This saves a lot of space, especially for large batches.
5The Batch Header, Field by Field
Every batch starts with a fixed header of 61 bytes that tells the broker everything it needs to know without opening the records.
The current on-disk format is called message format version 2, and its magic value is 2. It was introduced with Kafka 0.11 together with idempotent producers and transactions. The header is fixed in size and is laid out in a strict order. Reading it is like reading the label of a parcel.
| Field | Size | Meaning |
|---|---|---|
| baseOffset | 8 bytes | Offset of the first record in the batch |
| batchLength | 4 bytes | Number of bytes in the rest of the batch, so the reader knows where the next batch starts |
| partitionLeaderEpoch | 4 bytes | The leader generation number when the batch was written |
| magic | 1 byte | The format version, which is 2 today |
| crc | 4 bytes | A CRC-32C checksum used to detect damaged data |
| attributes | 2 bytes | Bit flags for compression, timestamp type, and transaction markers |
| lastOffsetDelta | 4 bytes | Distance from the first offset to the last offset in the batch |
| baseTimestamp | 8 bytes | Timestamp of the first record |
| maxTimestamp | 8 bytes | Largest timestamp in the batch |
| producerId | 8 bytes | The identity of the producer, used for duplicate detection |
| producerEpoch | 2 bytes | A counter that fences off older producer instances |
| baseSequence | 4 bytes | Sequence number of the first record from this producer |
| recordsCount | 4 bytes | How many records are inside |
Why the checksum starts where it does
Look closely at the order of the fields. The checksum begins at the attributes field and covers everything after it. It deliberately does not cover the base offset or the partition leader epoch. This is a very smart design choice. When a batch arrives at the leader broker, the broker must stamp the real base offset and the current leader epoch into the header. If the checksum covered those two fields, the broker would have to recompute the checksum for every single batch. Because the checksum skips them, the broker can overwrite just those bytes and leave everything else untouched.
The producer does not know the final offset, because only the leader broker decides it. So the producer sends a batch with a placeholder base offset, and the leader fills in the truth. Keeping that field outside the checksum makes this cheap.
Picture a sealed exam envelope. A tamper-proof sticker covers the flap. The school office writes the roll number on the outside after the sticker is placed. Writing the number does not break the sticker. In the same way, the broker writes the offset without breaking the checksum.
The producerId, producerEpoch, and baseSequence fields are the heart of exactly-once features. They let the broker recognize that the same batch arrived twice, for example because of a network retry, and quietly ignore the copy. They live inside every batch, even if you never turned on transactions, because idempotence is on by default in modern Kafka clients.
6The Attributes Field: Tiny Switches with Big Effects
Sixteen bits, and only a handful of them are used. Each one changes how the whole batch behaves.
The attributes field is a row of on/off switches packed into two bytes. Understanding it explains a lot of Kafka behavior, from how compression works to how consumers skip aborted transactions.
Compression codec
Says whether the records are stored raw or compressed, and with which method: none, gzip, snappy, lz4, or zstd.
Timestamp type
Zero means the timestamps were set by the producer (create time). One means the broker stamped them when it received the batch (log append time).
Transactional flag
Marks the batch as part of a transaction, so readers in read-committed mode know its fate depends on a later commit or abort marker.
Control batch flag
Marks a special batch that carries a transaction marker instead of user data. Applications never see these records.
Delete horizon flag
Used by log compaction to remember when a deletion marker may finally be removed, so the cleaner can track it inside the batch.
Create time versus log append time
Timestamps are important because time-based retention and time-based lookups depend on them. With create time, the producer application writes the time it thinks the event happened. This is great for event data, but it means a badly set clock on a producer can put strange values into your log. With log append time, the broker overwrites the timestamp with its own clock when the batch arrives. This is more predictable but hides the real event time. The topic setting message.timestamp.type chooses between the two.
If a producer sends timestamps far in the past, retention may treat the segment as ancient and delete it almost at once. If it sends timestamps far in the future, the segment may appear young forever and never be deleted. Always sanity-check the clocks of your producers.
7Inside the Batch: How Individual Records Are Packed
After the header comes the list of records. Each one is packed as tightly as possible.
A record contains a key, a value, an optional list of headers, and some small bookkeeping numbers. The clever part is the use of variable-length integers, often called varints. A normal 32-bit number always takes four bytes, even if the number is tiny. A varint uses only as many bytes as it needs: small numbers take one byte, and larger numbers take more. Since most lengths and deltas in real data are small, this saves a great deal of space when you have millions of records.
Length
The total size of the record, so a reader can jump to the next record quickly.
Attributes
One byte that is currently unused and kept for future needs.
Timestamp delta
How much later this record is compared to the batch base timestamp. Small numbers, so few bytes.
Offset delta
How far this record is from the batch base offset. The first record has 0, the next has 1, and so on.
Key length and key
The size of the key followed by the key bytes. A special length value means there is no key at all.
Value length and value
The size of the payload followed by the payload bytes. Kafka does not care what is inside; it may be JSON, Avro, Protobuf, or anything else.
Headers
A count, then for each header a key and a value. Headers carry side information such as trace IDs without touching the payload.
Delta encoding is like giving directions by saying “three blocks past the last stop” instead of reading out a full street address each time. The full address is given once, at the start of the batch. Everything after that is a short hop from it.
There is no separate storage for the record length in the header of the batch, which is why a reader must walk through the records in order. The batch header already tells you how many records to expect, and each record tells you how long it is, so walking is easy and exact.
Why deltas matter for big batches
A batch of one thousand records with millisecond spacing stores one full eight-byte timestamp and one thousand tiny deltas. Without deltas, it would store one thousand full eight-byte timestamps. Multiply that by billions of records per day and the saving becomes real money in disk and network cost.
8Compression: Shrinking the Parcel, Not the Label
Kafka compresses whole batches, and the header always stays readable.
When compression is turned on, the producer compresses only the records part of the batch. The header stays in plain form. That way the broker can read the base offset, the record count, the timestamps, and the producer identity without decompressing anything. The attributes field tells everyone which codec was used, so a consumer knows how to unpack the records later.
Because the whole batch is compressed as one block, bigger batches usually compress better. Repeated field names in JSON, repeated keys, and similar values across records all give the compressor more patterns to find. This is why a producer with a slightly longer linger.ms often sees smaller network traffic and smaller disks, even though each message waits a few extra milliseconds.
Imagine vacuum-packing a full suitcase of clothes. The label on the outside stays readable, and the clothes inside are squeezed together. Packing ten shirts together squeezes out more air than packing one shirt at a time.
What the broker does with compressed data
In the normal case, the broker does not decompress and recompress. It checks the batch header, verifies the checksum, assigns the offset, and appends the bytes as they came. This is the fast path and it is the reason a broker can handle very high throughput on modest hardware. There are only a few cases where the broker must open the batch and rebuild it:
- The topic has a compression setting that forces a different codec than the producer used.
- The topic uses log append time, so the timestamps inside must be rewritten.
- The batch uses an older message format and must be converted.
Each of these costs extra CPU on the broker. When you see a broker’s CPU climbing unexpectedly, mismatched compression settings between producers and topics are a very common cause.
| Codec | Speed | Space saved | Typical use |
|---|---|---|---|
| none | Fastest | None | Already compressed data such as images |
| lz4 | Very fast | Moderate | General purpose, latency-sensitive |
| snappy | Very fast | Moderate | General purpose |
| zstd | Fast | High | Storage-heavy topics, best balance for many teams |
| gzip | Slowest | High | Rarely the best choice for new systems |
Keep the topic-level compression setting at “producer” so that the broker keeps whatever the producer chose. Let the application teams pick the codec that fits their data, and avoid forcing conversions on the broker.
The batch size limit
The topic setting max.message.bytes limits the size of a batch, measured after compression. Its name is a leftover from old days when a batch held a single message. If a producer builds a batch bigger than this, the broker rejects it. Consumers and follower replicas must also be able to fetch a batch of this size, so if you raise the limit you must raise the matching fetch limits too.
9How the Format Grew Up: Versions 0, 1, and 2
The on-disk format has changed over the years, and each change solved a real problem.
The magic byte inside every batch is a version stamp. Looking at the history helps you understand why version 2 looks the way it does.
Version 0: the original
Messages had an offset, a size, a checksum, a key, and a value. There was no timestamp at all, so time-based features were impossible.
Version 1: timestamps arrive
A timestamp was added to each message, which made time-based retention and time-based search possible.
Version 2: batches become first-class
Introduced with Kafka 0.11. The batch header now carries producer identity, sequence numbers, and transaction flags. Records use varints and deltas. Headers were added to records.
What version 2 fixed
Duplicates on retry
Older versions could not tell a retried message from a new one. Producer ID and sequence numbers solved this.
Wasted space
Old messages repeated large fields per message. Deltas and varints shrank the size a lot.
Nested compression
Old formats wrapped compressed messages inside a wrapper message, which forced the broker to rewrite offsets by decompressing. Version 2 has a flat, simple layout.
No place for metadata
Nowhere to put trace IDs or routing hints. Record headers fixed this cleanly.
Modern Kafka releases have dropped support for the oldest message formats, so very old client programs may no longer work against a current cluster. When you plan an upgrade, check the client versions first, because a client that cannot speak the current format cannot talk to the broker at all.
10The Offset Index: A Sparse Shortcut into the Log
To read message number 1,000,000 without scanning a gigabyte, Kafka keeps a small map next to each log file.
Suppose a consumer asks for offset 4,517,210. The broker first chooses the right segment by looking at the base offsets. But inside a segment that may be a full gigabyte, it still needs to find the exact spot. Scanning from the start would be painfully slow. So each segment has an offset index, stored in the .index file.
Sparse on purpose
The index does not list every message. It adds one entry only after a certain number of bytes have been appended since the last entry. That number is controlled by index.interval.bytes, which defaults to 4096 bytes. So roughly every four kilobytes of data, one small entry is added. This is called a sparse index.
Think of the thumb tabs on the side of a dictionary. There is no tab for every word. There is a tab for each letter. You flip to the “M” tab, then read a few pages until you find your word. That is far faster than starting at page one, and the tabs take almost no space.
What one entry looks like
Each entry is exactly eight bytes. Four bytes hold the relative offset, which is the offset minus the segment base offset. The other four bytes hold the physical position, which is the byte location in the .log file where a batch starts. Using relative numbers keeps them small enough for four bytes, and this is one of the hidden reasons segments are limited in size: the position must fit in a four-byte number, so a segment must stay below about two gigabytes.
The arithmetic is simple. A full one-gigabyte segment holds about 262,144 chunks of four kilobytes. At eight bytes each, that is about two megabytes of index. This is tiny compared to the data, and that is why the index can be memory-mapped without stress.
sequenceDiagram
participant C as Consumer
participant B as Broker
participant S as Segment list
participant I as Offset index
participant L as Log file
C->>B: Fetch from offset 4517210
B->>S: Which segment holds this offset?
S-->>B: Segment with the closest smaller base offset
B->>I: Find largest entry not above the target
I-->>B: Byte position in the log
B->>L: Scan forward from that position
L-->>B: Batch containing the target offset
B-->>C: Return batches
How a lookup works
The broker keeps the list of segments sorted by base offset in memory. To find a segment, it looks for the largest base offset that is not bigger than the requested offset. Then it does a binary search inside that segment’s index, which means it repeatedly halves the search range, to find the largest indexed entry that does not go past the target. That entry gives a byte position. The broker starts reading the log from there and walks a few batches forward until it reaches the batch containing the target. Since entries are only four kilobytes apart, the walk is short.
Memory mapping and preallocation
The index file is memory-mapped, which means the operating system lets the program treat the file like a normal block of memory. The broker preallocates the file at its maximum size, controlled by segment.index.bytes with a default of ten megabytes. When a segment is closed, the file is trimmed to its real size. This is why an active segment’s index file looks bigger than expected: it is a reserved space, not real data. Operators who see ten-megabyte index files on many partitions sometimes panic, but it is normal.
Kafka can rebuild any index from the log file, because the index holds no unique information. If an index file is missing or damaged, the broker simply recreates it at startup. This is a big reason why Kafka treats the log as the only source of truth.
11The Time Index: Finding Messages by the Clock
Offsets answer “which message number”. Timestamps answer “what happened around 3 PM yesterday”.
Sometimes people do not know an offset. They know a time. A team may want to replay everything since a bad deployment at a certain hour. Kafka supports this through the time index, stored in the .timeindex file. Each entry is twelve bytes: an eight-byte timestamp and a four-byte relative offset.
An entry means “by the time we reached this offset, the largest timestamp we had seen so far was this.” The word largest is important. Real timestamps do not always increase, because different producers have different clocks. To make searching possible, the time index is always kept in increasing order by only recording the maximum timestamp seen so far.
Pick the segment
The broker finds the first segment whose largest timestamp is at least the requested time.
Search the time index
Inside that segment, a binary search finds the entry that matches the timestamp and gives a relative offset.
Use the offset index
That offset is turned into a byte position through the offset index, and the log is scanned from there.
The time index also drives retention
Time-based retention needs to know how old a segment is. Kafka uses the largest timestamp found in the segment for this, and that value is available from the time index without reading the log. A segment becomes eligible for deletion only when its newest record is older than the retention limit. This is the exact reason why a message can live longer than the retention period: the segment must be fully expired first.
A box of old photographs is thrown away only when the newest photo in the box is older than ten years. It does not matter that the first photo is fifty years old. The whole box goes at once.
Because time lookups depend on timestamps, a producer that sends wrong timestamps can make “find by time” return surprising results. The tool still works exactly as designed. The data it was given was the problem.
12The Read Path: From Consumer Request to Bytes on the Wire
Let us follow one fetch request from start to finish and see every file it touches.
Request arrives
The consumer sends its position, the partition, and a maximum number of bytes it is ready to accept.
Limits are checked
The broker checks that the offset is between the log start offset and the log end offset. If it is outside, the client gets an out-of-range error and must reset.
Segment and index lookup
The broker picks the segment and uses the offset index to find a byte position, as described earlier.
Visibility limit
A normal consumer may only read up to the high watermark, meaning messages that all in-sync replicas have copied. In read-committed mode the limit is even lower, called the last stable offset.
Send file region
The broker hands a slice of the log file directly to the network layer. No copy into the application memory takes place in the ordinary case.
Client unpacks
The consumer checks the checksum, decompresses the batch, and hands the records to the application.
A subtle point: whole batches only
The broker does not cut a batch in half. If a consumer asks for an offset in the middle of a batch, the broker returns the whole batch, starting from its first record. The client library then quietly throws away the records that come before the requested offset. This detail explains why the consumer sometimes receives slightly more data than it asked for on the wire, and it keeps the broker’s work extremely simple.
Since the broker never needs to look inside a healthy compressed batch, reading is mostly a matter of moving file bytes to the network. This is the core reason Kafka reads are cheap, and why a single broker can serve many consumers from the same data.
flowchart TD
A["Fetch request with offset"] --> B{"Offset in valid range?"}
B -- "No" --> C["Return out of range error"]
B -- "Yes" --> D["Find segment by base offset"]
D --> E["Search offset index for byte position"]
E --> F["Limit by high watermark or last stable offset"]
F --> G["Send file region to network"]
G --> H["Consumer verifies and decompresses"]
13The Write Path and the Art of Rolling Segments
New data always goes to the end of the active segment, until a rule says it is time to start a new one.
What happens on append
When a producer’s batch reaches the partition leader, the broker checks the batch for basic sanity: the format version, the size limit, the checksum, and whether it is a duplicate according to the producer sequence. If all is well, the broker assigns the next offsets, writes the base offset and the leader epoch into the header, and appends the bytes to the active segment. It also adds an entry to the offset index and the time index whenever the byte gap or timestamp rules say so. Then the followers copy the same bytes. Followers store exactly the same batches, byte for byte, which is how all replicas end up identical.
Notice that a write goes first into the operating system’s page cache, which is spare memory used by the operating system to speed up file access. Kafka does not force each write to the physical disk immediately. We will discuss why that is safe in the durability chapter.
The rolling rules
Rolling means closing the active segment and creating a new one whose base offset equals the next offset to be written. Kafka rolls a segment when any of these things happens:
Size limit reached
The next batch would push the segment above segment.bytes, which defaults to one gigabyte.
Time limit reached
The segment has been open longer than segment.ms, which defaults to seven days. A small random jitter is added so that many partitions do not roll at once.
Index is full
The offset index or the time index has no room for another entry.
Offset gap too wide
The distance between the segment’s base offset and the next offset no longer fits in four bytes.
A notebook is closed either when it is full of pages, or when the school term ends, even if some pages are empty. Kafka closes a segment for size or for age, whichever comes first.
What the roll actually does
On a roll, the broker flushes the old segment’s data to disk, trims the old index files to their true sizes, saves a snapshot of the producer state, and creates a new set of files named after the new base offset. From then on, the old segment is read-only. This is the moment when the old segment becomes eligible for retention and compaction rules.
A topic with very few messages may take weeks to fill one gigabyte. Its active segment stays open, and the active segment can never be deleted by retention. That is why small topics can hold data far longer than the retention time suggests. The segment.ms setting is the cure, since it forces a roll by age.
14Idempotence and Transactions: Control Batches, the Transaction Index, and Snapshots
Exactly-once behavior leaves footprints in the segment folder. Here is what each one does.
Duplicate detection and the producer snapshot
Every idempotent producer gets an ID from the cluster and numbers its batches in order. The leader remembers, for each producer and partition, the recent sequence numbers it has accepted. If the same batch arrives again after a network timeout, the leader sees the old sequence number and refuses to write it a second time. The broker keeps information about the most recent few batches from each producer, which is why the client limits how many unacknowledged requests may be in flight at once.
This producer state lives in memory, but memory is lost at restart. To avoid re-reading the whole log to rebuild it, the broker saves a producer state snapshot to a .snapshot file whenever a segment rolls. After a restart, the broker loads the latest snapshot and only replays the log after that point.
Control batches: the invisible markers
A transaction lets a producer write to several partitions and then either commit or abort all of it together. Data records are written right away, like normal ones, but flagged as transactional. When the producer finishes, the transaction coordinator writes a control batch into each partition involved. It contains a single marker that says commit or abort. Control batches take up offsets in the log, which is why you sometimes see gaps in the offsets a consumer receives. Client libraries never hand control records to your application.
Think of a bank cheque written in pencil. Everyone can see it is there, but it is only real once a bank clerk stamps it “approved” or “cancelled”. The stamp is the control batch. Readers who only want approved cheques wait for the stamp.
The transaction index
A consumer in read-committed mode must skip data from aborted transactions. To do that quickly, the broker keeps a transaction index in the .txnindex file. Each entry lists an aborted transaction with the producer ID, the first offset, the last offset, and the last stable offset at the time. When a read-committed fetch happens, the broker attaches the list of aborted transactions for the returned range, and the client filters those records out. A segment that has never seen an aborted transaction has no such file at all.
Last stable offset
The last stable offset is the position below which every transaction is finished. Read-committed consumers may not read past it. A single long-running transaction can hold this position back and make consumers appear stuck, even though the log keeps growing.
graph LR
A["Data batch: transactional"] --> B["Data batch: transactional"]
B --> C["Control batch: commit marker"]
C --> D["Data batch: transactional"]
D --> E["Control batch: abort marker"]
E --> F["Transaction index records the aborted range"]
15The Leader Epoch Checkpoint: Keeping Replicas Honest
Copies of a partition must agree. This small text file helps them agree after a leader change.
Each time a new broker becomes leader for a partition, the partition’s leader epoch number goes up by one. Every batch is stamped with the epoch that was current when it was written. The leader-epoch-checkpoint file lists each epoch together with the first offset written under it. It is a tiny text file with a version line, a count line, and then one line per epoch.
Why is this needed? Imagine the old leader wrote a few messages that were never copied to the followers, and then it crashed. A follower becomes the new leader and accepts different messages at the same offsets. When the old leader comes back, its log has offsets that mean something different from the new leader’s log. The returning broker asks the new leader, “Where did my last epoch end according to you?” The answer tells it exactly where to cut its own log, so it truncates the unsafe tail and then catches up. Without epochs, brokers would have to guess using only lengths, and a wrong guess could silently keep different data at the same offset.
Two classmates copy the teacher’s notes. The teacher changes the notes midway through the term and hands out “Edition 2”. A student who was absent shows up with a notebook that has a few extra lines from Edition 1. By comparing edition numbers and page numbers, the student knows exactly which lines to cross out.
When truncation happens, the broker cuts the log file at the right position, removes index entries that point past the cut, and may delete whole segments that are entirely beyond the cut. Truncation is the only case where data inside a segment is ever removed from the end.
16Retention: How Old Segments Leave the Building
Kafka never deletes single messages under the delete policy. It deletes whole segments, in a careful sequence.
The three retention limits
With the delete cleanup policy, a background task checks the closed segments on a regular schedule. The check interval is controlled by log.retention.check.interval.ms, which defaults to five minutes. A closed segment may be removed for any of these reasons:
Age limit
Controlled by retention.ms. The default at broker level is seven days. The newest record in the segment must be older than the limit.
Partition size limit
Controlled by retention.bytes. By default there is no limit. When the partition grows past it, the oldest segments are removed.
Log start offset moves
When someone deletes records up to a chosen offset through the admin tools, segments that lie entirely below that offset are removed.
The gentle two-step deletion
Deleting a file that a reader may still be using can cause trouble. So Kafka does it in two steps. First, the segment is taken out of the in-memory list, so no new reader will find it, and its files are renamed with a .deleted suffix. Second, after a delay controlled by file.delete.delay.ms, which defaults to one minute, the files are physically removed by a background thread. This means disk space is not returned at the exact moment retention decides to delete. There is a short gap.
Retention check finds an expired segment
The newest record is older than the limit, and the segment is not the active one.
Segment removed from the lookup list
New fetches can no longer see it. The log start offset moves forward.
Files renamed with the deleted suffix
Any reader still holding the file can finish safely.
Files erased after the delay
The disk space finally returns to the operating system.
The most common reasons are: the data is in the active segment, the segment still holds one recent record so it is not fully expired, or producers used odd timestamps. Check these three before blaming the broker.
17Log Compaction and the Temporary Files It Creates
Some topics keep only the latest value for each key. Segment files are the tool that makes it possible.
With the compact cleanup policy, Kafka keeps at least the latest record for every key, and older records with the same key may be removed. This is useful for things like the current profile of each user or the current price of each product. The log is split into a clean part, which has already been compacted, and a dirty part, which has not yet been processed. The active segment is never compacted.
How the cleaner rewrites segments
A background group of cleaner threads picks the partition with the highest share of dirty data, controlled by min.cleanable.dirty.ratio. It builds an in-memory map from each key to its latest offset by scanning the dirty part. Then it copies the older segments into new ones, keeping only records that are still the latest for their key. Several small old segments can be merged into one new segment during this rewrite.
Write cleaned copy
New files are written with a .cleaned suffix, so a crash never leaves a half-finished file that looks real.
Mark ready to swap
When the copy is complete, the files are renamed with a .swap suffix.
Swap in the new segment
The new segment replaces the old ones in the segment list, and the old ones are scheduled for deletion.
Rename to final names
The swap suffix is removed. If a crash happens midway, the broker finishes the swap at startup.
Details that surprise people
- Offsets never change. Records that survive compaction keep their original offsets. So compacted segments have gaps in offset numbers, and the index still works because it stores real positions.
- Tombstones. A record with a key and an empty value means “delete this key”. The tombstone itself stays for a while, controlled by
delete.retention.ms, so that slow consumers can see the deletion before it disappears. - Empty batches may stay. The last batch of a producer may be kept even if all its records are removed, so that duplicate detection information is not lost.
- Compaction is not instant. Old values can still be read until the cleaner reaches them.
Think of a class attendance sheet that is updated each day. Instead of keeping every day’s sheet forever, you keep only the latest status for each student. The cleaner is the person who tidies the old sheets into one final sheet, without ever changing the students’ roll numbers.
flowchart LR
A["Old segments with duplicate keys"] --> B["Cleaner builds key to latest offset map"]
B --> C["Copy only latest records into .cleaned files"]
C --> D["Rename to .swap"]
D --> E["Replace old segments"]
E --> F["Delete old files after delay"]
18Crash Recovery and the Truth About Durability
Kafka does not flush every write to disk, yet it survives crashes. Here is how.
The role of the page cache
When the broker writes to a file, the operating system first places the bytes in the page cache and writes them to the physical disk later. If only the Kafka process dies, the operating system still holds the bytes and no data is lost. Data can be lost only if the whole machine loses power before the cache is written. Kafka’s answer is not frequent flushing, which would be slow, but replication. A write is normally acknowledged only after enough in-sync replicas have received it, so the chance that every copy is lost at the same instant is tiny.
Instead of carefully carrying one precious document to a locked vault every minute, you give photocopies to three trusted friends in different cities. It is faster, and it is safer against a single accident.
What happens after an unclean shutdown
When a broker is stopped cleanly, it writes a small marker file called .kafka_cleanshutdown and flushes everything. On the next start, it sees the marker and loads quickly. If the marker is missing, the broker assumes a crash and enters recovery. It reads the recovery point from a checkpoint file, which marks the offset up to which data is known to be safely on disk. Every segment beyond that point is checked.
Find the unflushed segments
Segments after the recovery point are marked for checking.
Walk each batch and verify checksums
The broker reads batch after batch and confirms that each one is complete and its checksum is correct.
Cut off the damaged tail
At the first bad or half-written batch, the file is truncated, and any later segments are removed.
Rebuild the indexes
Offset and time indexes are recreated to match the surviving log, and producer state is rebuilt from the latest snapshot.
Catch up from the leader
The follower then fetches whatever it lost from the current leader.
Recovery work is split over threads, controlled by num.recovery.threads.per.data.dir. Brokers with many partitions and slow disks benefit from raising it. It also helps to avoid too many tiny segments, since each segment adds recovery work.
Settings such as flush.messages and flush.ms exist, but they usually hurt throughput and are unnecessary when replication is set up properly. Use a sensible replication factor, keep min.insync.replicas above one for important data, and have producers wait for full acknowledgement.
19Why the Format Is So Fast: Sequential I/O, Page Cache, and Zero-Copy
The file format was designed hand in hand with how operating systems work.
Sequential writes
Appending to the end of a file is the fastest thing a disk can do. Even spinning disks handle it well, because the head barely moves.
Page cache as the cache
Kafka does not build its own big cache. It trusts the operating system to keep recent data in memory. Recent data is exactly what most consumers read, so reads are served from memory.
Same bytes everywhere
The disk format equals the network format. The broker can send stored bytes to the network without translation.
Zero-copy transfer
The operating system moves data from the page cache straight to the network card, skipping copies into the application’s memory.
Imagine a mail room where boxes are already packed in the exact shape needed for delivery. The workers just slide the boxes onto the truck. Compare that with a mail room where every box must be opened, repacked, and resealed before shipping. Kafka is the first kind.
When zero-copy does not apply
Zero-copy works when the broker can send file bytes as they are. It is lost when the broker must touch the data, for example when it has to encrypt traffic with TLS, because encryption needs the bytes inside the application. In older releases it was also lost when converting message formats for old clients. That does not mean TLS is a bad idea. It means you should plan more CPU capacity on brokers when you turn it on.
graph LR
A["Producer batch"] -->|"network"| B["Leader broker"]
B -->|"append same bytes"| C["Log file in page cache"]
C -->|"same bytes"| D["Follower replicas"]
C -->|"zero-copy send"| E["Consumers"]
Because reads rely on the page cache, leave plenty of free memory to the operating system on a broker machine, and do not give all of it to the Java heap. A modest heap and a large page cache is the winning recipe.
20Tiered Storage: When Old Segments Move Away
Segments are such a clean unit that they can be moved to cheaper storage without changing how clients read.
Keeping months of data on fast local disks is expensive. Tiered storage lets a broker keep only recent segments locally and copy older closed segments to a remote system, such as cloud object storage. The feature was proposed as a Kafka improvement named KIP-405 and reached production readiness in release 3.9.
Only closed segments are eligible for copying. The active segment always stays local. Once a segment is safely stored remotely, the local copy may be removed according to a separate local retention limit, while the overall retention limit decides when the remote copy dies. Consumers keep using the same protocol. When they ask for an old offset, the broker fetches the needed piece from the remote store on their behalf.
Advantages
- Much cheaper long retention
- Brokers hold less local data, so adding or replacing a broker is faster
- Storage and compute can be sized separately
- No change needed in consumer applications
Trade-offs
- Reads of old data are slower than local reads
- An extra system must be operated and monitored
- Compacted topics and some settings have restrictions
- Remote storage costs and request charges must be watched
Why segments made this easy
Closed segments never change. That makes them perfect to copy as whole objects. If the design had used one endless file, copying only the old part would have been much harder.
21Looking Inside: Inspecting Segments with the Dump Tool
Kafka ships with a command line tool that reads segment files and prints what is inside.
The tool is named kafka-dump-log. You point it at a .log, .index, .timeindex, .txnindex, or .snapshot file, and it decodes the bytes into readable lines. For a log file, it prints one line per batch showing the base offset, last offset, record count, producer ID, epoch, sequence, whether the batch is transactional, the compression codec, the batch size in bytes, and the checksum. With an extra option it can also print the actual key and value of each record. For index files, it can check that the entries are sane.
Confirm compression
See which codec producers really use, without trusting anyone’s configuration notes.
Check batch sizes
Very small batches with only one record each reveal a producer that is not batching properly.
Verify indexes
After a suspicious crash, verify that an index matches its log.
Investigate transactions
Find control batches and the aborted ranges that block read-committed consumers.
Run the tool on a copy of a file, or on a file from a closed segment, rather than on the active one during heavy traffic. Never edit segment files by hand. Also remember that printing record values can expose sensitive business data, so limit who can run it.
22The Key Settings That Shape Segments
A few settings decide almost everything about how your segment files behave.
| Topic setting | Broker default name | Default | What it controls |
|---|---|---|---|
segment.bytes | log.segment.bytes | 1 GB | Maximum size of one segment before it rolls |
segment.ms | log.roll.ms or log.roll.hours | 7 days | Maximum age of the active segment before it rolls |
segment.index.bytes | log.index.size.max.bytes | 10 MB | Reserved size of the index files |
index.interval.bytes | log.index.interval.bytes | 4 KB | Data gap between index entries |
retention.ms | log.retention.hours | 7 days | How long closed segments are kept |
retention.bytes | log.retention.bytes | No limit | Maximum partition size before old segments go |
file.delete.delay.ms | log.segment.delete.delay.ms | 1 minute | Delay between marking and erasing files |
max.message.bytes | message.max.bytes | About 1 MB | Largest batch the broker accepts |
message.timestamp.type | log.message.timestamp.type | Create time | Who sets the timestamps |
cleanup.policy | log.cleanup.policy | Delete | Delete old segments or compact them |
Thinking about segment size
Choosing the segment size is a trade-off between two forces. Larger segments mean fewer files, fewer open file handles, and less overhead. But they also delay deletion and compaction, since only closed segments qualify. Smaller segments give finer control over retention and quicker cleaning, but they multiply the number of files, and each partition needs at least three open files per segment. For a busy topic, the default one gigabyte is a good starting point. For a low-traffic topic, reduce the roll time so segments close even when they are small.
Estimate the number of segments a broker will hold by adding up all partition sizes and dividing by the segment size. Multiply by three for open files, and make sure the operating system’s open file limit and memory-map limit are comfortably above that number.
23Monitoring: Watching Segments Stay Healthy
You do not need to look at files every day if the right numbers are on a dashboard.
Log size
Shows how many bytes each partition uses. Sudden growth means a producer surge or retention that is not working.
Number of segments
A very high count hints at tiny segments or a very small roll setting, which raises file handle pressure.
Log start and end offsets
The gap between them is the number of messages currently retained. A stuck start offset points to blocked deletion.
Log flush rate and time
Slow flushes reveal a struggling disk long before users complain.
Cleaner health
Watch the longest cleaning time and how full the cleaner buffer is. A dead cleaner thread silently stops all compaction.
Offline log directories
Counts disks that have failed. Anything above zero needs immediate attention.
Operating system numbers to watch
- Disk free space on every log directory, with an alert well before it is full.
- Open file descriptors compared with the allowed limit.
- Memory-map count, because each index file uses one, and the system has a limit.
- Disk latency and utilization, to detect slow devices.
- Page cache hit behavior, seen indirectly through disk read rates. Consumers that read old data cause disk reads that push out hot data.
A broker that runs out of memory maps or file handles can crash even when disks are half empty. This happens when someone lowers the segment size on a huge number of partitions. Always test such changes on a small group first.
24Security Around Segment Files
Segment files hold your raw business data, so they deserve the same care as a database file.
Encrypt the disk
Kafka does not encrypt segment files by itself. Compression is not encryption. Use disk or volume encryption from the operating system or cloud provider.
Lock down the directory
Only the broker’s service account should read or write the log directories. Anyone who can read the files can read every message.
Use TLS between machines
Protects data moving between clients and brokers and between brokers. Remember the CPU cost and the loss of zero-copy.
Use ACLs
Control who may read and write each topic, so that fewer people ever need file-level access at all.
Deleting sensitive data
Because Kafka is append-only, personal data does not disappear the moment you decide it should. It disappears when its segment is deleted, or when compaction rewrites it. Teams under privacy laws must plan for this. Options include short retention for sensitive topics, compacted topics with tombstones and a short tombstone retention time, or encrypting personal fields inside the message with keys that can be thrown away later. The last approach makes data unreadable even while the bytes still sit in old segments or backups.
Disk snapshots and copies of log directories contain the same plain bytes. Protect and expire them with the same rules as the live data.
25Design Trade-offs of the Segment Format
Every design choice has a price. Here is the honest balance sheet.
Advantages
- Very fast writes because everything is appended at the end
- Cheap deletion of whole segments
- Small, rebuildable indexes that fit in memory
- Same bytes on disk and on the network, enabling zero-copy
- Simple recovery: check the tail, cut it, rebuild indexes
- Immutable closed segments that are easy to copy, tier, and compact
Disadvantages / Trade-offs
- Retention works per segment, so data can outlive its limit
- No in-place edits or deletes of single messages under the delete policy
- Many partitions mean many files and many open handles
- Lookup by offset needs a short scan after the index step
- Plain data on disk needs external encryption
- Whole batches are returned, so a little extra data crosses the wire
| Question | Small segments | Large segments |
|---|---|---|
| Retention precision | Better, data leaves closer to its limit | Coarser, data may linger |
| Open files and memory maps | Many more | Fewer |
| Compaction responsiveness | Faster, more segments become cleanable | Slower, the active part is bigger |
| Restart and recovery | More files to check | Fewer files but larger ones |
| Best for | Low-traffic topics needing timely deletion | High-traffic topics |
26Anti-patterns to Avoid
These mistakes appear again and again in real clusters.
Problem
A team sets the segment size to a few megabytes on hundreds of busy partitions, hoping for very precise retention.
Why It’s Harmful
The number of segment files explodes. Each one needs open file handles and memory-mapped indexes. The broker hits system limits, restarts become slow because of huge recovery work, and background threads spend their time rolling and deleting instead of serving clients.
Correct Approach
Keep segments reasonably large on busy topics. If precision matters, use a modest roll time on the specific topics that need it, and verify limits before rolling the change out.
Problem
Someone deletes or moves files inside a partition folder by hand while the broker is running, to free disk space quickly.
Why It’s Harmful
The broker holds an in-memory picture of the segments and open handles to files. Removing files behind its back can cause read errors, a failed log directory, or silent inconsistency between replicas. Space may not even be freed because open handles keep deleted files alive.
Correct Approach
Free space by lowering retention, deleting records through the admin tools, removing unused topics, or adding disks. Let the broker manage its own files.
Problem
Forcing a flush after every message, in the belief that it is the only way to protect data.
Why It’s Harmful
It throws away Kafka’s biggest speed advantage, the page cache, and gives a false sense of safety, because a single machine can still be lost completely.
Correct Approach
Rely on replication with a sensible replication factor, a minimum in-sync replica count, and full acknowledgements from producers.
Problem
Trusting producer clocks blindly while using time-based retention on a shared cluster.
Why It’s Harmful
A producer with a clock set far ahead can create segments that never expire. A producer with a clock set far behind can cause data to be deleted immediately.
Correct Approach
Use log append time on topics where broker-side time is more trustworthy, or validate timestamps at the source, and use the topic settings that limit how far timestamps may differ from the broker’s clock.
27Best Practices and Common Mistakes
A short checklist that turns the theory of this tutorial into daily habits.
Do This
- Keep default segment size for busy topics unless you have measured a reason to change it
- Set a roll time on quiet topics so retention can work
- Let producers choose compression and keep the topic setting at producer
- Give brokers plenty of free memory for the page cache
- Alert on disk free space, open files, and offline log directories
- Use separate disks or volumes sized with headroom for growth and recovery
- Test upgrades and setting changes on a small set of topics first
Do Not Do This
- Do not edit or delete segment files by hand
- Do not expect exact-to-the-minute deletion of old messages
- Do not depend on flush settings for safety
- Do not ignore very small batches, they waste space and CPU
- Do not run the disk almost full, since a full disk can take a broker down
- Do not forget that backups hold the same sensitive bytes
When something about storage looks wrong, check in this order: is the topic’s cleanup policy what you think, is the data in the active segment, what are the largest timestamps, is the cleaner thread alive, and finally what does the dump tool say about the files. This sequence solves the majority of cases.
28Real-World Situations Explained Through Segments
Let us apply everything to situations that engineers meet in production.
Case 1: “We set retention to one day, but data from last week is still there”
The topic is quiet, and its active segment has been open for a week because it never reached one gigabyte. The active segment cannot be deleted. Adding a roll time of one day or less closes segments by age, and after the next roll, retention can finally remove old ones.
Case 2: “The disk filled up even though retention is configured”
A burst of traffic produced far more data than the size limit assumed, or a single hot partition grew quickly. Retention only runs every few minutes and only removes closed segments, so growth between checks can overwhelm a small disk. The fix is headroom, a size-based limit as a safety net, and alerts on growth rate.
Case 3: “The broker takes forever to restart after a crash”
The broker is recovering many segments after an unclean shutdown. Each unflushed segment must be scanned and its indexes rebuilt. Raising the number of recovery threads and avoiding very small segments both shorten the wait. Clean shutdowns skip most of this work.
Case 4: “Consumers in read-committed mode seem stuck”
A transaction is still open, so the last stable offset is not moving. The log keeps growing, but read-committed readers cannot go beyond the open transaction. The cure is to find and fix the producer that never commits or aborts, and to set sensible transaction timeouts.
Case 5: “A compacted topic still shows old values”
The old values live in segments that the cleaner has not reached, or in the active segment, which is never cleaned. Compaction is a background process, not a real-time one. Consumers must always expect to see some older values for a key until the cleaner catches up.
Case 6: “CPU on the brokers jumped after we changed a topic setting”
The topic now forces a compression codec that differs from what producers use. Every batch is decompressed and compressed again on the broker. Setting the topic back to accept the producer’s choice removes the extra work.
Kafka began at LinkedIn as a way to move large streams of activity data reliably, and the segmented log was central to its design from the beginning. Today, teams in many industries use the same format for event sourcing, change data capture, metrics pipelines, and log aggregation. The reason the design has survived is exactly what this tutorial showed: it is simple, and simple things are fast, recoverable, and easy to reason about.
29A Worked Example: The Numbers Behind One Busy Partition
Let us put real numbers on everything we learned, so the design feels concrete instead of abstract.
Imagine a partition that receives one thousand records every second, and each record carries about two hundred bytes of key and value. To keep the arithmetic friendly, we will ignore compression and small overheads for the moment. The partition therefore grows by about two hundred thousand bytes every second.
How fast do segments roll?
One gigabyte is a little over one billion bytes. Dividing that by two hundred thousand bytes per second gives roughly five thousand four hundred seconds, which is about ninety minutes. So this partition closes a segment about every hour and a half. In seven days, which is one hundred sixty-eight hours, that means about one hundred thirteen segments. Each segment has three regular files, so the partition folder holds around three hundred forty files, and the folder holds about one hundred twenty-one gigabytes of data.
How big are the indexes?
A full segment holds about 262,144 blocks of four kilobytes. Each block earns one offset index entry of eight bytes, so a full offset index needs about two megabytes. The time index uses twelve bytes per entry, so at most about three megabytes. Together, the helper files add around five megabytes to a one-gigabyte segment. That is half of one percent, a tiny cost for fast lookups.
How much does batching matter?
Suppose the producer waits five milliseconds before sending. In that time about five records collect for the partition, so it sends roughly two hundred batches per second. A batch of five records carries the fixed 61-byte header once, plus the five records with their small overheads. The header cost is spread across five records, a bit over twelve bytes each. Now imagine the producer sends every record alone. Each record carries its own 61-byte header, which is about a quarter of its total size. Over a week, that waste adds up to tens of gigabytes on this single partition, and it also multiplies the number of requests, checksums, and index updates.
| Producer behavior | Records per batch | Header cost per record | Batches per second |
|---|---|---|---|
| No waiting, one record at a time | 1 | 61 bytes | 1000 |
| Wait about 5 ms | About 5 | About 12 bytes | About 200 |
| Wait about 20 ms | About 20 | About 3 bytes | About 50 |
A few milliseconds of patience on the producer side saves disk, network, CPU, and index work everywhere else. Batching is the cheapest performance win in the whole system, and it shows up directly in the size of the segment files.
The life of one record, start to finish
Born in the producer
The application hands the record to the client library, which places it in a batch for the right partition and waits for the batch to fill or the wait time to end.
Compressed and sent
The batch is compressed, its checksum is computed, and it is sent to the partition leader.
Stamped and appended
The leader validates the batch, writes the real base offset and leader epoch, and appends it to the active segment.
Copied to followers
Followers fetch the same bytes. Once enough in-sync replicas have them, the high watermark moves past the batch and consumers may read it.
Served to consumers
The batch is found through the segment list and offset index, then sent, often straight from the page cache.
Segment closes
After about ninety minutes, the segment rolls and becomes read-only.
Retention removes it
Roughly seven days after the newest record of that segment was written, the whole segment is renamed, then erased.
30Partition Folders Through Their Whole Life
A partition folder is created, copied, moved, and finally removed. Segments play a role at every step.
Creation
When a topic is created, the cluster decides which brokers will hold each partition and its copies. Each chosen broker creates the folder, an empty first segment with base offset zero, and the small metadata files. The first segment’s files are created immediately, even before a single message arrives.
Replicas do not have to look identical
It is easy to assume that every replica of a partition contains exactly the same segment files. In truth, all replicas contain the same batches at the same offsets, but the way those batches are grouped into segments can differ. Each broker rolls its own segments according to its own clock and its own size counting, and each broker runs compaction on its own schedule. A replica that joined late and caught up by fetching a large amount of history may have cut its segments at different places than the leader. This is perfectly fine, because clients only care about offsets, and offsets agree everywhere.
Three students copy the same story into three different notebooks. All three notebooks contain every sentence in the same order, but one student starts a new notebook every 30 pages, and another one starts a new notebook every Monday. The story is identical. Only the notebook boundaries differ.
Adding a broker or moving a partition
When a partition is reassigned to a new broker, the new broker becomes a follower. It fetches the log from the leader beginning at the leader’s log start offset, and writes what it receives into its own segments. Nothing is copied file by file. It is an ordinary replication fetch, just a very long one. This is why moving a partition with hundreds of gigabytes takes hours, and why throttling the replication traffic is important, so that regular clients keep good performance while the move runs. Tiered storage reduces this cost, since less of the history lives on local disks.
Failing disks and several directories
Brokers can use several log directories, one per disk. If one directory fails, for example because of disk errors, the broker marks it offline. The partitions in that directory go offline on this broker, and leadership for them moves to other replicas. The rest of the broker keeps running from the healthy directories. Operators can then replace the disk and let the broker rebuild the missing replicas by fetching from other brokers. Monitoring the count of offline directories is therefore very important.
Deleting a topic
When a topic is deleted, each broker renames the partition folder by adding a delete marker and a unique ID to its name, and then removes it in the background. The rename is quick and makes sure a new topic with the same name can be created straight away without clashing with the folder that is still being erased.
Do not move a partition folder between directories by hand to balance disks. Use the supported administrative tools that move replicas between directories, because they keep the broker’s in-memory view and the checkpoint files consistent.
flowchart TD
A["Topic created"] --> B["Partition folder and first segment created"]
B --> C["Segments roll and grow over time"]
C --> D["Old segments removed by retention or compaction"]
C --> E["Replica moved to another broker"]
E --> F["New replica rebuilds segments by fetching"]
C --> G["Topic deleted"]
G --> H["Folder renamed with delete marker"]
H --> I["Folder erased in the background"]
31How the Segment Log Compares with Other Storage Designs
Understanding what Kafka chose not to do makes its choices easier to remember.
Most databases store data in structures built for fast random updates, such as trees of pages that are changed in place. That design is wonderful when you need to change a single row and read it back instantly. But it needs a lot of random disk movement, careful locking, and complex repair work after a crash. Kafka chose a very different goal. It wants to accept an endless stream of facts as fast as possible and let many readers replay them in order. For that goal, an append-only log is the natural shape.
| Question | Kafka segment log | Update-in-place database | Traditional message queue |
|---|---|---|---|
| How is data written? | Appended to the end only | Pages changed anywhere | Often appended, then marked as consumed |
| Can an old record be edited? | No, only replaced by a newer record and compacted | Yes | Usually no |
| What happens when a consumer reads? | Nothing is removed, the reader just keeps a position | Rows stay in place | Message is often removed after delivery |
| Can new consumers replay history? | Yes, until retention removes it | Only through extra history tables | |
| How is old data removed? | Whole segments, on a schedule | Row deletes and cleanup | On acknowledgement |
| Main strength | Throughput and replay | Flexible queries and updates | Routing and per-message handling |
The consumer position is not in the segment
In many queue systems, the queue itself tracks which messages were consumed. In Kafka, the segment files know nothing about consumers. The position of each consumer group is stored separately, in an internal topic that is itself a log made of segments. Because of this, adding a tenth consumer group costs the log nothing. It only adds another position number. This separation is why one topic can serve dozens of independent applications at once.
A library book does not know who has read it. Each reader keeps their own bookmark. The library only has to keep the book on the shelf. Kafka’s segments are the books, and the consumer positions are the bookmarks.
What you gain, what you give up
You gain very high throughput, easy replay, many independent readers, and simple recovery. You give up cheap updates of single records, complex queries, and instant deletion. Knowing this trade helps you decide when Kafka is the right tool, and when it should sit beside a database instead of replacing it.
32A Plain-Words Glossary of Segment Terms
Keep this table nearby when you read Kafka documentation or debug a storage problem.
| Term | Plain meaning |
|---|---|
| Partition | One ordered, append-only list of records within a topic |
| Segment | One slice of a partition’s log, made of a data file and helper files |
| Active segment | The newest segment, the only one accepting writes |
| Base offset | The offset of the first record in a segment or a batch |
| Record batch | A group of records stored and sent together with one header |
| Record | A single message with key, value, and headers |
| Magic byte | The format version number stored in each batch |
| Attributes | Bit flags in a batch header for compression, timestamp type, and transactions |
| Varint | A number stored using only as many bytes as it needs |
| Delta | A small difference from a base value, used to save space |
| Sparse index | An index with an entry only every so often, not for every record |
| Offset index | Maps offsets to byte positions in the log file |
| Time index | Maps timestamps to offsets |
| Transaction index | Lists aborted transactions so readers can skip their data |
| Control batch | A special batch that holds a commit or abort marker |
| Producer snapshot | A saved copy of producer state for duplicate detection |
| Leader epoch | A counter that goes up each time leadership changes |
| High watermark | The offset below which all in-sync replicas have the data |
| Last stable offset | The offset below which all transactions are finished |
| Log start offset | The oldest offset still available in the partition |
| Log end offset | The offset that the next new record will receive |
| Recovery point | The offset up to which data is known to be safely on disk |
| Retention | The rule that decides how long closed segments are kept |
| Compaction | Keeping only the latest record for each key |
| Tombstone | A record with a key and no value, meaning “delete this key” |
| Page cache | Memory used by the operating system to hold recently used file data |
| Zero-copy | Sending file data to the network without copying it through application memory |
33Frequently Asked Questions
Short answers to the questions people ask most often about segment files.
It is one slice of a partition’s log, made of a data file and its helper index files, all named after the offset of the first message in the slice. A partition is a chain of such slices.
The name is the base offset padded to twenty digits, so that alphabetical order equals offset order.
You will see mostly unreadable bytes, since it is a binary format, and compressed batches will look like noise. Use the dump tool to read it properly.
Index files are preallocated to their maximum size while the segment is active. They are trimmed to real size when the segment closes.
The broker rebuilds it from the log file when it starts, because the log is the only source of truth. The rebuild takes time on large partitions.
Gaps come from transaction control markers, from compaction removing older records, or from aborted transactions. The offsets themselves never get reused.
No. It deletes whole closed segments once the newest record in them is old enough, and it checks on a schedule, so deletion can lag.
For the process crashing, yes, because the operating system still holds the bytes. For a whole machine failing, safety comes from having other replicas with the same data.
No. Byte positions in the index are four-byte numbers, so a segment must stay under about two gigabytes.
Batches are returned whole. If your requested offset falls inside a batch, you receive the entire batch and the client drops the earlier records.
It is the only one that receives writes, and it cannot be deleted by retention or compacted by the cleaner until it rolls.
Yes. Compaction only rewrites closed segments into new ones, keeping the same batch format and the original offsets of surviving records.
34Summary and Key Takeaways
A Kafka partition is a folder. That folder holds a chain of segments, each named after the offset of its first message. Inside each segment, a plain data file stores record batches one after another, and two small sparse indexes, one by offset and one by time, make lookups quick. Extra files record transaction facts, producer state, and leader history. Only the newest segment accepts writes, and once it rolls, it becomes immutable and eligible for retention, compaction, and tiering. Every part of this design serves the same goal: do the least possible work on the hot path, and make failure recovery simple and safe.
Key Takeaways
- A partition is a directory of segments — each segment is a family of files named by a twenty-digit base offset.
- The .log file is only record batches — a 61-byte header per batch, then compact records using deltas and varints, with compression applied to the records part only.
- Indexes are sparse and rebuildable — an eight-byte offset entry roughly every four kilobytes and a twelve-byte time entry, so lookups are a binary search plus a short scan.
- Segments roll by size, age, or index limits — and only closed segments can be deleted, compacted, or moved to tiered storage.
- Retention works on whole segments — the newest timestamp in a segment decides its fate, so data can outlive its limit and bad timestamps cause surprises.
- Durability comes from replication, not constant flushing — recovery checks the unflushed tail, cuts damage, and rebuilds indexes, while leader epochs keep replicas consistent.
- Speed comes from matching the operating system — sequential appends, the page cache, and one byte layout from producer to disk to consumer enable zero-copy sends.