The beginner lesson drew a partition as one tape of records numbered from 0. On the broker, the server that stores the partition, that tape is a directory. A topic orders with 3 partitions puts 3 directories under the broker's log.dirs path, named orders-0, orders-1 and orders-2. Inside each directory the records sit in segments, and a segment is a small group of files named by the offset of the first record it holds. The listing below is orders-0 after 2.4 million records of about 1 KiB each.
What orders-0 holds on disk
| file | what is inside |
|---|---|
| 00000000000000000000.log | records 0 to 999,999, about 1 GiB |
| 00000000000000000000.index | offset to byte position, sparse |
| 00000000000000000000.timeindex | timestamp to offset, sparse |
| 00000000000001000000.log | records 1,000,000 to 1,999,999 |
| 00000000000001000000.index | its offset index |
| 00000000000001000000.timeindex | its time index |
| 00000000000002000000.log | records 2,000,000 onward, still growing |
| 00000000000002000000.index | its offset index |
| 00000000000002000000.timeindex | its time index |
Each .log name is the base offset of its segment, padded with zeros so the names sort as text in offset order. That number lets the broker pick the right file for any offset without opening one: to serve a read at 1,500,000 it takes the largest base offset at or below the target, which is 1000000, and goes to that segment. Offsets 0 to 999,999 live in the first file, and a reader positioned at 1,500,000 never touches it.
Finding one offset inside a 1 GiB file
The second file, .index, maps offsets to byte positions inside the .log, and it is sparse: the broker adds an entry after every few kilobytes of appended data, not after every record. A lookup for 1,500,000 binary searches the index for the nearest entry at or below the target, seeks to that byte position in the .log, and scans forward a few kilobytes until it reaches the record. The .timeindex maps a timestamp to the offset of the first record at or after it, so a reader that asks for records since 09:00 is answered with an offset and then served through the .index like any other read. Neither index is the data: if one is missing or damaged the broker rebuilds it from the .log when it starts.
The active segment
Only the newest segment is written, and it is called the active segment. Every append goes to the end of its .log, and its 2 index files grow with it. When the active segment reaches segment.bytes, 1 GiB by default, or has been open for segment.ms, 7 days by default, the broker rolls: it closes the 3 files and opens a new segment whose base offset is the next offset to be written. A closed segment is never written again. Nothing is appended to it, and no record inside it is changed in place; the only things that ever happen to it are deletion as a whole and, on a compacted topic, being rewritten into a new file.
Segment size sets how much a partition can delete at once
| bytes written per day to the partition | what rolls the segment | segment size at roll |
|---|---|---|
| 50 GiB | segment.bytes, about 50 times a day | 1 GiB |
| 1 GiB | segment.bytes, about once a day | 1 GiB |
| 2 MiB | segment.ms, once a week | 14 MiB |
- ▸The number of .log files is the number of segments, and the one with the largest base offset is the active segment.
- ▸A .log name is the first offset it holds; the next .log name is 1 past the last offset it holds.
- ▸A closed .log far smaller than segment.bytes was rolled by segment.ms, which tells you the partition was quiet.
- ▸The .log alone is the data; the .index and .timeindex can be rebuilt from it.
kafka-dump-log.sh --files /var/lib/kafka/orders-0/00000000000001000000.log --print-data-log prints every record of that segment with its offset, size and timestamp, read straight from the file. Point it at a closed segment when you want to see a partition exactly as the broker keeps it.Retention is the broker deleting old segments on a schedule, and 2 topic settings drive it. retention.ms is how long a record may stay, 7 days by default. retention.bytes caps the bytes a partition may hold, and its default of -1 means no cap. Both are enforced per partition, not per topic: a topic with 12 partitions and retention.bytes of 1 GiB holds about 12 GiB, plus up to 1 segment per partition on top, as the size rule below shows. Reading plays no part at all. A record fetched 1,000 times and a record never fetched expire at the same moment.
A topic-level retention.ms overrides the broker-wide log.retention.ms and log.retention.hours, so set it on the topic, where the number sits next to the data it governs.
The time rule reads one timestamp per segment
Every log.retention.check.interval.ms, 5 minutes by default, a background thread visits each partition and walks its segments from oldest to newest. For each one it takes the largest timestamp the broker recorded among the records inside. If the clock minus that timestamp is more than retention.ms, the segment is deleted whole. The walk stops at the first segment young enough to keep, since later segments hold later records. If the walk reaches the active segment and it is past the window too, the broker rolls a fresh empty segment first and then deletes the old one, so a partition always keeps at least 1 segment and an empty one is never deleted.
| segment | oldest record | newest record | age of newest at Thu 09:10 | result with retention.ms = 3 days |
|---|---|---|---|---|
| 00000000000000000000 | Fri 14:00 | Mon 08:00 | 3 days and 1 hour | deleted |
| 00000000000000420000 | Mon 08:01 | Wed 10:00 | 23 hours | kept, with its Monday records |
| 00000000000000910000 (active) | Wed 10:01 | growing | inside the window | kept |
The second segment keeps its records from Monday 08:01, which are past the window, because the rule reads only the newest timestamp in each segment and judges the whole segment by it. Those records leave on Saturday 10:00, or at the first check after it, when the segment's newest record turns 3 days old. The time rule alone never removes a record before retention.ms, and it rarely removes one at retention.ms. When retention.bytes is also set, the size rule can take a segment sooner, whatever its age.
Four reasons the window is never exact
- The segment rulewhole files go, or stayA record leaves when the newest record in its segment is old enough. In a 1 GiB segment that can be hours or days after the record's own timestamp passed the window.
- The check interval5 minutes by defaultA segment that becomes old enough at 09:01 waits for the check at 09:05, and longer if the interval has been raised on the broker.
- The record's timestampthe record's own clockAge is measured from the timestamp the record carries, so the window is counted from the record's stamp rather than from the moment the broker wrote it.
- The delete delayfile.delete.delay.msA deleted segment is first renamed with a .deleted suffix and its files are unlinked after file.delete.delay.ms, 1 minute by default, so the disk space comes back a minute after the check.
The size rule is a ceiling that overshoots
retention.bytes is checked against the partition's total bytes, active segment included. A partition that has written less than the cap keeps everything. Once it passes the cap, the thread removes the oldest segment only while the partition would still hold at least retention.bytes after the removal, so deletion stops at the last point at or above the cap and the partition can sit up to 1 segment over it. With a 2 GiB cap and 1 GiB segments, a partition holding 3 closed segments and 512 MiB in the active one has 3.5 GiB. Removing the oldest leaves 2.5 GiB, still over the cap, so it goes. Removing the next would leave 1.5 GiB, under the cap, so it stays. The partition settles at 2.5 GiB and will not shrink further until more is written.
When both settings are on, a segment goes as soon as either rule says so. Neither limit is exact: the time rule removes a segment only after its newest record is older than retention.ms, and the size rule holds a partition at no more than retention.bytes plus 1 segment.
A reader whose position sits inside a deleted segment cannot resume there: the records it was about to fetch are gone, and the earliest offset the partition can serve has moved up to the base offset of the oldest surviving segment. Lowering retention.ms on a live topic takes effect at the next check, so look at where the slowest reader stands before changing it.
- Size retention.bytes per partition and multiply by the partition count for the topic.
- Give short-retention topics smaller segments so expiry tracks the window.
- Leave headroom in retention.ms for the longest outage a reader may have.
- Expect disk to come back at the moment retention.ms passes.
- Count on reading to free space; only the clock and the byte cap do.
- Lower retention.ms on a live topic without checking the slowest reader.
Some topics carry state rather than events. A topic of customer addresses keys each record by customer id and carries the current address as the value. The 5th address c-7 sent makes the earlier 4 worthless, but the delete policy would also drop the 5th once it turned 7 days old, and a reader building a table of addresses would then find c-7 missing. cleanup.policy=compact changes what a partition keeps: for each key, the record with the highest offset is never removed, and older records with the same key may be. A reader starting at offset 0 receives every key's latest value, however old it is, and the partition's size tracks the number of distinct keys rather than the number of writes.
One partition, before and after a cleaning pass
| offset | key | value |
|---|---|---|
| 0 | c-7 | Oslo |
| 1 | c-2 | Lima |
| 2 | c-7 | Bergen |
| 3 | c-9 | Quito |
| 4 | c-2 | Cusco |
| 5 | c-7 | Tromso |
| 6 | c-4 | Pune |
| 7 | c-9 | Loja |
| offset | key | value |
|---|---|---|
| 4 | c-2 | Cusco |
| 5 | c-7 | Tromso |
| 6 | c-4 | Pune |
| 7 | c-9 | Loja |
The pass left 4 records, 1 per key, and each kept the offset it was written at. Offsets 0 to 3 no longer exist, and a reader that asks for offset 1 is handed the record at 4, the next offset the partition has. The survivors stay in their original order, so c-2 at 4 still comes before c-7 at 5. Compaction never creates or moves a record; it only leaves some out when it rewrites a segment.
Where the cleaning stops
| part of the partition | segments | what a key looks like there |
|---|---|---|
| cleaned head | closed segments the cleaner has rewritten | at most 1 record per key |
| uncleaned tail | closed segments not yet visited, plus the active segment | every write, repeats and all |
Cleaning is per partition, so the promise of 1 latest record per key holds only when every record for a key lands in the same partition, which the key itself arranges and the last section returns to.
What a compacted topic demands
- ▸Every record must carry a key. The broker rejects a keyless record with an invalid record error.
- ▸Per key, the record at the highest offset is kept. Only older records for that key can go.
- ▸Offsets are stable. Gaps appear where records were removed, and a reader skips over them.
- ▸The order of surviving records is the order they were written.
- ▸The tail is uncleaned, so one key can appear many times to a reader.
The policy is set per topic, at creation with kafka-topics.sh --create --bootstrap-server localhost:9092 --topic customer-addresses --partitions 6 --config cleanup.policy=compact or later with kafka-configs.sh. A topic may carry both policies, cleanup.policy=compact,delete: the cleaner keeps the latest value per key, and the retention thread still removes segments whose newest record is older than retention.ms, so a key that stops being written vanishes after the window.
- Every record is an event and every one matters
- Size is bounded by time or by bytes
- A reader starting at 0 sees the last window
- Keys are optional
- Each record is the state of one key
- Size is bounded by the number of keys
- A reader starting at 0 sees every key's latest value
- Keys are required
Why the tombstone must linger
Suppose the cleaner removed the tombstone in the same pass that removed the old values. A reader that was halfway through the partition, or offline for the night, would reach the end and never learn that c-7 was deleted; its table would carry a stale address with nothing left in the topic to correct it. So the tombstone is kept after the values are gone, for delete.retention.ms, 24 hours by default, measured from the moment the cleaner first processed the segment that holds it. A later pass after that time removes it. A reader that reads through within the window sees the tombstone and deletes the key. A reader that arrives after it sees no record for c-7 at all, and that is also correct: the key is absent.
| moment | what happens to c-7 | what a reader starting at 0 ends with for c-7 |
|---|---|---|
| Mon 10:00 | tombstone written at offset 812, in the active segment | Oslo at 0, Bergen at 2, Tromso at 5, then the tombstone: no c-7 |
| Tue 02:00 | the segment holding 812 rolls and closes | no c-7 |
| Tue 03:00 | the cleaner rewrites that segment: offsets 0, 2 and 5 are dropped, 812 is kept and stamped with Tue 03:00 | only the tombstone: no c-7 |
| Wed 03:05 | a pass finds 812 older than delete.retention.ms and drops it | nothing for c-7: no c-7 |
Each row ends in the same state for a reader that starts from offset 0, which is the point of the design. The window matters for a reader that already holds c-7 from an earlier read and comes back later: if it returns before Wednesday 03:05 it sees the tombstone and deletes the key; if it returns after, nothing in the topic mentions c-7, and the stale row survives in its table.
What a reader does with each record
| record | reader's action on its table |
|---|---|
| key with a value | insert the key, or overwrite the value it already holds |
| key with a null value | delete the key |
| a key seen twice in the tail | apply both in offset order; the later one is the state |
A reader that skips null values, which some deserializers tempt it to do by failing on an empty payload, keeps every deleted key. Processing in offset order makes the rest work without special cases: in the Monday row above the tombstone sits in the active segment behind 3 live values the cleaner has not touched, and a reader applying each record in turn still finishes with c-7 removed.
What a tombstone does not do
- ▸On a topic with the delete policy, a null value means nothing to the broker; the record stays until retention removes its segment.
- ▸A tombstone removes nothing by itself. The older values go when the cleaner next rewrites their segments.
- ▸A tombstone does not reset a key. Writing c-7 again later appends a new record at a new offset, and the key is back.
- ▸A tombstone in a different partition from the key's values removes nothing, because cleaning is per partition.
Spreading across brokers
A key's partition depends on the count
A record with a key goes to the partition at hash(key) mod partition count, and the Java client computes that hash with murmur2. With 3 partitions, cust-7 maps to the same partition every time, which is what lets a compacted partition hold the key's latest value and lets a reader see the key's records in order. Change the count to 4 and hash mod 4 sends cust-7 somewhere else. The animation sends the same 8 records, 4 keys, to 2 topics side by side: orders with 3 partitions and orders-v2 with 4, or with 6 at the switch.
Follow 1 key across the 2 topics. In orders its 2 records land together, and in orders-v2 they land together again, though some keys sit on a different partition number than they do in orders. Either way a key's records stay together; only the directory that holds them changes. If orders were a topic's past and orders-v2 its future at a higher count, a key that moved would be split across 2 directories.
Adding partitions moves nothing
kafka-topics.sh --alter --bootstrap-server localhost:9092 --topic orders --partitions 4 creates 1 new empty directory on a broker and tells producers that there are now 4 targets. No existing record moves, because the segments are closed files and compaction and retention are the only things that rewrite them. A key's history now lives in 2 places: its past in the partition it used to hash to, its future in the partition it hashes to now. For that key, 2 things break. Its order across the change is gone, since a reader working 2 partitions can be anywhere in one when it reads the other. And on a compacted topic, the old partition keeps the key's old value forever; a tombstone sent later goes to the new partition and removes nothing in the old one, so a reader rebuilding a table from offset 0 can finish with the stale value, depending on which partition it reads last.
Picking the number
| topic | what drives the count | a count that fits |
|---|---|---|
| 30 MB/s of order events, readers at 4 MB/s | reader parallelism, with headroom | 12 |
| an audit trail that must read in 1 global order | order | 1, and the throughput of 1 partition |
| a compacted table of 2 million keys | spreading keys across brokers, fixed for the topic's life | chosen once, with growth built in |
- Measure one partition's throughput and divide the target by it.
- Create with room to grow; 12 partitions on a small topic cost little.
- Treat a compacted topic's count as fixed; grow it with a new topic.
- Add partitions to a topic whose readers depend on per-key order.
- Create 1,000 partitions to be safe; each is a directory of files on a broker.
- Plan on shrinking a topic; the count only goes up.
> LinkedIn's profile-edit topic and its page-view topic run on the same cluster. The profile topic is compacted and keyed by member id, so the search index rebuilds every member's current headline from offset 0, and a tombstone follows an account closure so that an index rebuilt 3 weeks later shows no trace of it. The page-view topic keeps 7 days with 256 MiB segments, which its traffic fills every few hours, so disk returns within hours of the window. Both counts were chosen at creation, because the profile topic's keys can never be rehashed without stranding old headlines in old partitions.
Apache Kafka is an open-source distributed event streaming platform used by thousands of companies for high-performance data pipelines, streaming analytics, data integration, and mission-critical applications.
kafka.apache.org
- Segments on Disk
- Writessegment.bytes
- Retention by Time and Size
- retention.msretention.bytes
- Compacted Topics
- Log Compactioncleanup.policy
- Tombstones and Deletes
- Log Compaction Basicsdelete.retention.ms
- Choosing a Partition Count
- num.partitionsModifying topics
Apache Kafka, Kafka, and the Kafka logo are either registered trademarks or trademarks of The Apache Software Foundation in the United States and other countries.
Segments, retention, compaction, tombstones and partition counts: what a partition keeps, drops and cannot undo.
- Category
- Kafka
- Difficulty
- intermediate
- Duration
- 20 minutes
- Challenges
- 9 hands-on challenges
Topics covered: Segments on Disk, Retention by Time and Size, Compacted Topics, Tombstones and Deletes, Choosing a Partition Count
Lesson Sections
- Segments on Disk
What orders-0 holds on disk Finding one offset inside a 1 GiB file The active segment Segment size sets how much a partition can delete at once Because a closed segment is a sealed unit, the segment is the smallest amount of data a partition can give back. A partition with 1 GiB segments cannot drop 10 MiB of old records; it drops 1 GiB or nothing. How big a segment gets before it rolls depends on traffic, because the 2 settings race and whichever is reached first wins. A busy partition fills se
- Retention by Time and Size
A topic-level retention.ms overrides the broker-wide log.retention.ms and log.retention.hours, so set it on the topic, where the number sits next to the data it governs. The time rule reads one timestamp per segment Four reasons the window is never exact The size rule is a ceiling that overshoots
- Compacted Topics
One partition, before and after a cleaning pass Where the cleaning stops The work is done by the log cleaner, a background thread on the broker, and it rewrites closed segments only: it reads a closed segment, copies the records that are the latest for their key into a new segment file, and swaps the new file in for the old. The active segment is never cleaned, and a closed segment waits its turn, so at any moment a partition is a cleaned part at the front and an uncleaned tail at the back where
- Tombstones and Deletes
Compaction keeps the latest record for every key forever, so a customer who closes their account still has a current address in the topic a year later. There is no command that removes a key from a partition. The producer removes it by writing one more record: the same key with a null value, called a tombstone. To the cleaner the tombstone is the key's latest record, so the older values for that key are dropped on the next pass. To a reader it is an instruction: delete this key from whatever you
- Choosing a Partition Count
The partition count is set when a topic is created, and it decides order, parallel reading, placement and cleanup. Records are ordered within one partition and not across them. A partition is read in order by 1 reader at a time, so 8 partitions let 8 readers work at once while a 9th waits idle. Each partition is a directory on 1 broker, so the count decides how a topic's bytes and writes spread across the cluster. Expiry and compaction both work partition by partition too. Spreading across broke