Log Compaction Internals in Kafka

Log Compaction Internals in Kafka

How Kafka quietly keeps the latest value for every key without ever changing a single offset. Meet the cleaner threads, the offset map, tombstones, and every setting that matters, explained from zero.

Think about the contacts app on your phone. When a friend changes their number, you do not keep a long diary of every number they ever had. You keep the newest one, and the old one goes away. Most of the time, that is exactly what you want. Apache Kafka has a feature that does the same thing for data streams. It is called log compaction, and it powers some of the most important parts of Kafka itself, including the place where Kafka remembers how far every consumer has read.

This guide takes you inside that feature. You will learn what a Kafka log really is, why time-based deletion is sometimes the wrong tool, and how a small background worker called the log cleaner shrinks a log while keeping every promise Kafka makes to its readers. We will open up the offset map, follow one cleaning run step by step, watch a deleted key travel through its whole life, and finish with tuning, monitoring, failure handling, and design advice you can use in production.

1The Big Idea: What Log Compaction Is and Why It Exists

Compaction answers one question: what if I only care about the newest value for each thing, not the whole history?

Kafka is a system that stores streams of records. A record is a small package of data. Every record can have a key, a value, and a timestamp. The key is a label that says what the record is about. The value is the actual information. For example, a record might have the key user-42 and a value that holds that user’s email address.

Now imagine your app sends a new record every time user 42 changes their email. After a year, the stream contains hundreds of records with the key user-42. Only the last one describes what is true today. The other hundreds describe the past.

The problem with plain time-based deletion

By default, Kafka cleans up old data using a retention rule. For example: “delete anything older than seven days.” That is perfect for things like click events or sensor readings, where the past matters only for a short while.

But look at what happens to our user profile stream. Suppose user 42 changed their email once, ten days ago, and has not touched it since. The retention rule sees that record as older than seven days and deletes it. Now Kafka knows nothing about user 42, even though that record was the newest truth about them. The rule removed data by age, not by usefulness.

What compaction does instead

Log compaction changes the rule from “delete by age” to “delete by being replaced.” Kafka promises to keep at least the last known value for every key. Older values for the same key can be thrown away, because a newer one has taken their place. A quiet key that has not changed in a year stays safe, because nothing has replaced it.

Everyday Analogy

Picture a whiteboard in a school hallway that shows which classroom each teacher is in today. When a teacher moves rooms, someone erases the old room and writes the new one. Nobody keeps a pile of erased notes. The board always shows the latest room for every teacher, and a teacher who never moves keeps the same line forever. A compacted Kafka topic is that whiteboard, except it is made of an ordered list of updates, and a helper tidies it in the background.

flowchart TB
  subgraph B["Before compaction"]
    direction LR
    b0["offset 0, K1 = A"] --> b1["offset 1, K2 = B"] --> b2["offset 2, K1 = C"] --> b3["offset 3, K3 = D"] --> b4["offset 4, K2 = E"] --> b5["offset 5, K1 = F"] --> b6["offset 6, K3 = null"]
  end
  subgraph A["After compaction"]
    direction LR
    a4["offset 4, K2 = E"] --> a5["offset 5, K1 = F"] --> a6["offset 6, K3 = null, a tombstone"]
  end
  B -.->|cleaner runs| A
FIG 1 — Only the newest record per key survives. Offsets 4, 5 and 6 keep their original numbers.

Read the picture from left to right. Key K1 was written three times (A, C, then F), so only F remains. Key K2 was written twice (B, then E), so only E remains. Key K3 was written with a value D and then with null. A null value is special: it means “this key is deleted.” We call it a tombstone, and Chapter 8 explains its whole life story.

1+
latest value always kept for every live key
0
offsets ever renumbered by compaction
24 bytes
default cleaner memory used per unique key
i
Remember

Compaction is a background cleanup job, not a live database update. Right after you write a new value, the old one may still be sitting in the log for a while. The promise is about the final state, not about how fast the cleanup happens.

2Kafka Log Foundations You Need First

You cannot understand the cleaner without knowing what it cleans. Here is the storage layout in plain words.

Kafka stores data in topics. A topic is like a named folder for one kind of record, such as user-profiles or orders. To handle huge amounts of traffic, each topic is split into partitions. A partition is one ordered line of records, and it lives on disk as a log. Compaction works on one partition log at a time, never across partitions.

Building block

Record

One message: a key, a value, a timestamp, and optional headers. The key decides which partition the record goes to and what “same thing” means to the cleaner.

Building block

Partition

One ordered, append-only line of records. New records are added to the end. Nothing in the middle is edited in place.

Building block

Offset

A record’s permanent position number in its partition: 0, 1, 2, 3 and so on. Consumers use offsets to remember where they stopped reading.

Building block

Segment

A partition log is cut into files called segments. Each segment holds a range of offsets. A segment stops growing once it hits a size or age limit.

Building block

Active segment

The newest segment, the one still receiving writes. The cleaner never touches it, because it is still changing.

Building block

Index files

Small helper files next to each segment. They let Kafka jump straight to an offset or a timestamp without reading the whole file.

Everyday Analogy

Think of a partition as a very long roll of receipt paper. Each receipt has a number printed on it, and that number never changes. The roll is stored in chapters (segments) so it is easy to carry. Only the newest chapter is open on the counter where the cashier is writing. Everything older is closed and stacked on a shelf. The cleaner is a helper who only works with the closed chapters on the shelf.

How segments look on disk

Inside a partition folder you will see files named after the first offset they hold. A segment called 00000000000000368769.log starts at offset 368769. Next to it are .index and .timeindex files. The .log file holds the actual records in batches. Kafka groups records into batches before writing, because moving many records together is much faster than moving them one by one. Compaction has to respect this batch structure when it rewrites data.

Why the key matters so much

When a producer sends a keyed record, Kafka’s default partitioner hashes the key and picks a partition. The same key therefore always lands in the same partition, and it keeps its order there. This is the foundation of compaction. Since all records for user-42 sit in one partition in the exact order they were written, the cleaner can decide “which record is newest” just by looking at offsets in that one log. If the same key were spread over many partitions, no single cleaner could compare its records.

!
Important

A compacted topic does not accept records without a key. Kafka rejects them at write time, because “keep the latest per key” has no meaning when there is no key. Always make sure your producers set a meaningful, stable key.

3Delete, Compact, or Both: The Cleanup Policies

Every topic has a cleanup.policy setting that decides how old data leaves the log.

A topic can use one of three cleanup styles. The setting is called cleanup.policy at the topic level. Choosing the right one is the first design decision you make, so it helps to see them side by side.

PolicyWhat leaves the logWhat staysTypical use
delete (default)Whole segments older than the retention time, or beyond the retention sizeEverything newer than the limit, including repeated keysEvent streams, logs, metrics, clicks
compactOlder records whose key has a newer recordAt least the latest record for every key, foreverCurrent-state tables, configuration, offsets
compact,deleteReplaced records and also anything past the retention timeLatest record per key, but only within the retention windowWindowed state, bounded “latest” views

The default delete policy in numbers

With the delete policy, the broker checks whether whole segments are too old or the partition is too big. By default, the time limit is seven days (retention.ms of 604,800,000 milliseconds) and there is no size limit (retention.bytes of -1). The broker removes entire closed segments, never single records. This is cheap because deleting a file is quick.

Compaction is a different kind of work

Compaction cannot just delete whole files, because a segment usually contains a mix of records to keep and records to drop. It has to read the segment, filter out the losers, and write a new smaller file. That is real disk and CPU work, and it is why compaction has so many tuning settings while plain deletion has almost none.

The combined policy

You can set cleanup.policy to compact,delete. Then two rules apply at once. The cleaner removes replaced values, and the retention rule also removes anything that is older than the retention time, even if it is the latest value for its key. This is how Kafka Streams stores windowed data: “keep the latest per key, but forget windows older than the window retention.”

!
Watch out

If you switch a topic from compact to compact,delete with a short retention, latest values for quiet keys will start disappearing. That surprises many teams. Only add the delete part when forgetting old keys is truly what you want.

4The Compaction Contract: What Kafka Promises and What It Does Not

Good engineers know the guarantees by heart. Most production bugs come from expecting more than Kafka promised.

Compaction is safe to build on because it makes a clear set of promises. Understanding both the promises and the limits will save you from painful surprises.

The promises

Guarantee 1

Latest value survives

For every key, the newest record is never removed by the cleaner. A reader will always be able to find the final state of each key.

Guarantee 2

Order is preserved

Records that remain stay in their original relative order. Compaction never shuffles anything.

Guarantee 3

Offsets never change

A surviving record keeps the exact offset it was given at write time. Consumers’ saved positions stay valid after any cleaning.

Guarantee 4

Full snapshot from the start

A consumer that reads from the beginning to the end sees at least the last value of every key. That is enough to rebuild the complete current state.

Guarantee 5

Fresh readers see everything

A consumer that keeps up with the newest records sees every write, because the cleaner does not touch the newest part of the log.

Guarantee 6

Deletes are possible

A record with a key and a null value marks the key as deleted. After a grace period, the marker and all older values for that key disappear.

What it does not promise

Kafka does promise

  • The final value for each key is kept
  • Original offsets and order are kept
  • Tombstones stay visible for a minimum time
  • Eventually, replaced values are removed

Kafka does not promise

  • That old values vanish immediately
  • That the log has no duplicate keys at any moment
  • That offsets are gap-free (gaps are normal)
  • That the active segment is ever compacted while it is active
Compaction promises the final state of every key, not the full story of how it got there.

Gaps in offsets are normal

After compaction, a partition might hold offsets 4, 5, 6, 11, 12, and 20. The missing numbers belonged to replaced records. A consumer asking for offset 7 simply gets the next available record, offset 11. Well-written consumers never assume that offsets are consecutive. If your code counts records by subtracting offsets, it will be wrong on a compacted topic.

Duplicates in the newest part of the log

The cleaner only works on segments that are closed and old enough. So the newest part of the log can still hold many records for the same key. A consumer reading near the head will see all of them, in order. This is a feature, not a bug: it means live consumers get every change, while late starters get a compact snapshot.

5Architecture and Components of the Log Cleaner

Compaction is not a separate service. It is a small team of workers living inside every Kafka broker.

Each Kafka broker runs its own cleaning machinery inside the same program that serves producers and consumers. There is no extra server to install. When a broker starts, and cleaning is enabled (the default), it also starts a pool of cleaner threads. Let us meet each part of the team.

Coordinator

LogCleanerManager

The planner. It keeps track of which partition logs are eligible, calculates how “dirty” each one is, hands out work, and remembers which logs are being cleaned right now.

Worker pool

Cleaner threads

The workers. Each thread repeatedly asks the planner for the dirtiest log, cleans it, and asks again. The default is one thread, set by log.cleaner.threads.

Memory

Offset map

A fixed-size table in memory. It remembers, for each key hash, the newest offset seen in the part of the log being examined. It is the cleaner’s “who is the newest?” cheat sheet.

Bookmark

Cleaner checkpoint file

A small file called cleaner-offset-checkpoint in each log directory. It stores, per partition, the offset up to which cleaning has already been done.

Traffic control

I/O throttler

A speed limiter that keeps cleaning from eating all disk bandwidth. It is controlled by log.cleaner.io.max.bytes.per.second.

Data

Partition log and segments

The thing being cleaned. The cleaner reads closed segments, writes replacements, and swaps them in without stopping producers or consumers.

Everyday Analogy

Imagine a large library with many long shelves. The head librarian (the manager) walks the aisles with a clipboard and notes which shelf is the messiest. A tidying helper (a cleaner thread) is sent to that shelf. The helper first makes a small cheat sheet of the newest edition of each book from the newest boxes of returns (the offset map). Then the helper walks the whole shelf, removes outdated editions, and puts the survivors into a fresh, smaller shelf. At the end, the librarian updates a note on the clipboard saying how far the tidying went (the checkpoint).

flowchart TB
  P["Producers write keyed records"] --> AL["Active segment of a partition log"]
  AL --> CS["Closed segments on disk"]
  LCM["LogCleanerManager picks the dirtiest log"] --> CT["Cleaner thread"]
  CT --> OM["Offset map: key hash to newest offset"]
  CT --> CS
  CT --> NEW["Replacement segments, first .cleaned then .swap"]
  NEW --> CS
  CT --> CP["cleaner-offset-checkpoint file"]
  LCM --> CP
  C["Consumers read the log"] --> CS
  C --> AL
FIG 2 — Main components of the log cleaner inside one broker.

Where each replica fits in

Every partition is stored on several brokers as replicas: one leader and a few followers. Each broker cleans its own copy independently. There is no central cleaning command from the leader. Because compaction never changes offsets and only removes records that are provably outdated, every replica still ends up with the same latest value for every key, even if the exact timing of cleaning differs.

The three regions of a log

To talk clearly about the cleaner, we split a partition log into three regions, from oldest to newest.

flowchart LR
  S["Start of log"] --> CL["Clean region: already compacted, one record per key"]
  CL --> DR["Dirty region: newer closed segments, keys may repeat"]
  DR --> UN["Uncleanable region: active segment and protected recent segments"]
  UN --> H["Newest offset"]
FIG 3 — The three regions the cleaner reasons about.
  • Clean region: the older part that a previous run has already compacted. Inside it, each key appears at most once.
  • Dirty region: closed segments written after the last cleaning. The same key may appear many times here.
  • Uncleanable region: the active segment, plus any segments that are protected by a minimum compaction lag or by open transactions. The cleaner leaves these alone for now.

The Kafka documentation calls the old part the “tail” and the newest part the “head” of the log. In this guide we use clean and dirty because those words tell you what is inside.

How the planner picks a log: the dirty ratio

The planner computes a number for each log: the cleanable ratio, also called the dirty ratio. It is the size of the dirty region divided by the size of the clean plus dirty regions. A ratio of 0.5 means half of the cleanable data is new and unchecked. The setting min.cleanable.dirty.ratio (default 0.5) says how dirty a log must be before it is worth the effort. Among all logs above the limit, the planner picks the one with the highest ratio.

i
Why a threshold at all?

Cleaning rewrites data, and rewriting costs disk reads and writes. If only a tiny bit of new data has arrived, a whole run would copy a lot of old data just to remove very little. The ratio makes Kafka wait until the cleanup is worth its price.

6Inside One Cleaning Run, Step by Step

This is the heart of the guide. Follow one thread as it compacts one partition.

A cleaning run has two big phases: first learn who the newest records are, then copy the log while dropping everything that is not the newest. Here are the steps in order.

1

Choose a log

The manager finds the partition with the highest dirty ratio that is above the threshold. A log whose oldest dirty record is older than the maximum compaction lag is also eligible, even with a low ratio. If nothing qualifies, the thread sleeps for the backoff time (15 seconds by default) and tries again.

2

Find the boundaries

The cleaner reads the checkpoint to learn where the clean region ends. It then finds the first uncleanable offset. That is the smallest of: the start of the active segment, the start of any segment that is still younger than the minimum compaction lag, and the first offset of an open transaction.

3

Build the offset map

The cleaner scans the dirty region from oldest to newest. For every keyed record, it stores the key’s hash and the record’s offset in the map. If the key is already there, the newer offset simply overwrites the older one. At the end, the map holds the newest offset of every key in the scanned part.

4

Note how far the map reached

The map has a limited size. If the dirty region has more unique keys than fit, the cleaner stops when the map is nearly full and remembers the last offset it covered. The rest waits for the next run.

5

Copy with a filter

Starting from the very beginning of the log, the cleaner reads each batch of records up to the map’s limit. For each record it asks: “Is this the newest offset for its key, or is the key not in the map at all?” If yes, it keeps the record. If a newer offset exists in the map, the record is dropped.

6

Handle tombstones

Tombstones follow the same rule as other records, but with one extra step: once a tombstone has lived long enough (the delete retention time), the cleaner drops it too. Chapter 8 covers this fully.

7

Write in groups

Old segments are combined into groups. Each group is rewritten into one new segment, as long as the result stays under the segment size and the offsets fit the index limits. Small segments left by earlier runs get merged this way.

8

Swap safely

The new data is written to a temporary file ending in .cleaned. When it is complete, it is renamed to .swap, then the broker swaps it into the log in place of the old segments, and the old files are deleted in the background.

9

Move the bookmark

The checkpoint is updated to the offset where this run stopped. The next run will treat everything before it as clean.

Why the cleaner re-reads the clean region

This surprises many people. The map is built only from the dirty region, yet the copy phase starts at the very beginning of the log. Why touch data that was already cleaned? Because new dirty records can replace old clean ones. Imagine key K1 has a value in the clean region. Yesterday’s dirty segment holds a newer K1. The old K1 in the clean region is now outdated and must go. The only way to find it is to walk the clean region again and check every key against the map.

!
Cost to remember

Because every run re-reads the clean region, the work per run grows with the total log size, not just the amount of new data. Kafka reports this as the “recopy percentage.” A large compacted topic with rare cleaning pays a big cost each time, which is exactly why the dirty ratio threshold exists.

Everyday Analogy

Think of updating a big alphabetical phone directory with a new pile of change slips. First you sort the slips and write the newest number for each name on a cheat sheet. Then you flip through the entire directory from page one. Any name that appears on your cheat sheet with a newer number gets crossed out in the old directory, and you copy the good pages into a fresh, thinner directory. You even glue together thin leftover pages so the book stays neat. Finally you swap the books on the shelf in one move so nobody is left holding half a directory.

Rewriting batches without breaking producers

Kafka records live in batches, and each batch header carries information about the producer that wrote it, such as its ID and sequence numbers. These are used for idempotent writes and transactions. When the cleaner drops every record in a batch, it can still keep the empty batch shell for the latest batch of each producer, so the broker does not lose the producer’s sequence state. The cleaner also removes records from aborted transactions, since nobody should ever read them, and it drops old transaction marker records after they are no longer needed.

If batches were compressed, the cleaner has to decompress, filter, and compress again. This costs CPU, which is one more reason the cleaner runs in the background with its own throttle.

Producers and consumers keep working

Nothing in the cleaning run blocks writes, because writes only touch the active segment, which the cleaner ignores. Consumers reading old offsets keep reading the old files until the swap; after the swap, they continue from the same offsets on the new files. This works because offsets are stable, which is the single most important design choice behind compaction.

7The Offset Map: The Cleaner’s Memory

A tiny, clever hash table decides how much of your log can be cleaned in a single run.

The offset map is a fixed block of memory that acts like a hash table. It is nicknamed “skimpy” in the source code because it stores as little as possible. It does not keep your keys. It only keeps a fingerprint of each key plus an offset.

What one entry looks like

16 bytes
hash fingerprint of the key (MD5 by default)
8 bytes
newest offset seen for that key
24 bytes
total per unique key, no matter how large the key is

A key of 5 bytes and a key of 5,000 bytes both cost exactly 24 bytes in the map. This is a big memory saver when keys are long, such as full URLs or JSON strings.

How much can one run cover?

The total memory for the map is set by log.cleaner.dedupe.buffer.size, default 128 MB. It is divided equally among the cleaner threads. The map also refuses to be more than 90 percent full (the load factor from log.cleaner.io.buffer.load.factor), so lookups stay fast. With one thread and the defaults:

ItemValue
Buffer size134,217,728 bytes (128 MB)
Usable at 90 percent loadabout 120.8 million bytes
Bytes per entry24
Unique keys per runabout 5 million

So a single run can look at roughly five million unique keys in the dirty region. If your dirty region holds 50 million unique keys, the cleaner will need about ten runs to work through it. Doubling the buffer doubles the reach. Because many records in the dirty region repeat the same key, the number of records covered per run can be much greater than the number of unique keys.

i
Sizing rule of thumb

Take the number of unique keys you expect in the dirty region of your largest partitions, multiply by 24, and divide by 0.9. That is the buffer per thread you need to finish that partition in a single pass. Add headroom for growth.

Open addressing and collisions

The map uses a simple technique called open addressing. To store a key, it turns the fingerprint into a slot number. If the slot is taken by a different fingerprint, it tries the next slot, and the next, until it finds a free one. Lookups follow the same path. Since the fingerprints are 128 bits wide, the chance that two different keys share one is astronomically small. The design accepts that tiny risk in exchange for using far less memory than storing full keys.

Everyday Analogy

Imagine a coat check that does not keep your coat, only a small numbered ticket stub with your handwriting sample and the shelf number. The stub is tiny no matter how bulky the coat is. The clerk can tell you which shelf holds your latest coat from the stub alone. It is possible in theory for two people to have identical handwriting samples, but the sample is so detailed that it practically never happens.

What happens when one segment is too big for the map

The cleaner adds whole segments to the map as long as they fit. If even a single segment holds more unique keys than the empty map can take, the cleaner cannot make progress on that partition and reports an error. The fixes are to raise the buffer size, lower the segment size so each segment has fewer keys, or add cleaner threads only if you also add memory. This is one of the most common real-world compaction problems, and it is easy to miss until it shows up in monitoring.

Compaction speed is limited less by disk and more by how many unique keys fit in the map.

8Tombstones: How Deletion Works in a Compacted Log

You cannot erase a line from an append-only log, so Kafka deletes by writing a special note that means “this key is gone.”

A tombstone is a record that has a key but a null value. It tells the world that the key no longer exists. Note that an empty string is not null; a tombstone must have no value at all.

The life of a tombstone

flowchart LR
  W["1. Producer writes key with null value"] --> L["2. Tombstone appended as the newest record for the key"]
  L --> C1["3. Cleaner removes all older values of the key"]
  C1 --> KEEP["4. Tombstone is kept for the delete retention time"]
  KEEP --> C2["5. A later cleaning run drops the tombstone"]
  C2 --> GONE["6. Key has fully vanished from the log"]
FIG 4 — Six stages from delete request to complete removal.

Why not remove the tombstone immediately along with the old values? Because of consumers that keep their own copy of the data. Imagine a service that reads a compacted topic to fill a local cache. If it is offline while the cleaner removes both the old value and the tombstone, it will never learn the key was deleted. Its cache would hold a ghost entry forever. By keeping the tombstone for a grace period, Kafka gives every consumer time to see it.

The grace period: delete retention

The topic setting delete.retention.ms controls the grace period. Its default is 24 hours (86,400,000 milliseconds). A consumer that is away for less than that time is guaranteed to see the tombstone. In newer Kafka versions, the countdown begins when the cleaner first processes the tombstone in a cleaned segment, so the guarantee is a full window. Older versions measured from the last-modified time of the segment, which could shorten the real window.

!
Danger

If a consumer stays away longer than delete.retention.ms, it may miss deletes. When it returns, do not just continue from where it stopped. Rebuild its state by reading the topic from the beginning. Many Kafka Streams and Connect setups do this automatically after long gaps.

Tombstones can be deleted too, on purpose

Some teams want deleted data gone quickly, for privacy or storage reasons. They lower delete.retention.ms. This is fine if all consumers are always online or rebuild after downtime. It is risky otherwise. The choice is a trade-off between deletion speed and consumer safety, discussed further in Chapter 13.

Producing a tombstone

Every Kafka client library offers a way to send a record with a key and a null value. Kafka Streams turns a null update of a table row into a tombstone. Debezium, the change-data-capture tool, emits a delete event followed by a tombstone by default, so that compacted topics can forget the row.

9Data Flow and Lifecycle: One Key from Birth to Vanishing

Let us follow a single key through three moments in time to see exactly what compaction does.

Suppose a topic called user-profiles has one partition. Our story covers users u1, u2 and u3. The table shows what the log holds at three moments.

Moment 1: before any cleaning

OffsetKeyValueNote
0u1city = Punefirst write
1u2city = Delhifirst write
2u1city = Mumbaiu1 moved
3u3city = Goafirst write
4u1city = Chennaiu1 moved again
5u3nullu3 deleted

Moment 2: after the first cleaning run

The cleaner builds its map: u1 maps to offset 4, u2 to offset 1, u3 to offset 5. It then copies the log and keeps only records that match the map.

OffsetKeyValueWhy it stayed
1u2city = Delhinewest and only record for u2
4u1city = Chennainewest record for u1
5u3nulltombstone, kept during the grace period

Offsets 0, 2 and 3 are gone. The offsets 1, 4 and 5 kept their numbers. A consumer reading from offset 0 now jumps to offset 1, then 4, then 5.

Moment 3: after the grace period and another run

OffsetKeyValueWhy it stayed
1u2city = Delhistill the newest for u2
4u1city = Chennaistill the newest for u1

The tombstone at offset 5 has been dropped, and u3 has completely vanished from the topic. Any consumer that read the log during the grace period knows u3 was deleted. Any new consumer never even hears about u3, which is exactly right.

What different readers experience

Reader A

Live consumer

Reads each record right after it is written. It saw every step: Pune, Mumbai, Chennai for u1, and the delete of u3. Nothing was lost for it.

Reader B

New consumer after moment 3

Starts at offset 0 and sees only u2 and u1 with their final values. That is a perfect snapshot of the current state.

Reader C

Consumer paused at offset 2

Resumes and continues at the next existing offset, 4. Its saved position remained valid because offsets never move.

Reader D

Consumer offline for weeks

May have missed the u3 tombstone if the grace period passed. It should rebuild from the beginning to stay correct.

10Configuration Guide: Every Knob That Matters

Settings come in two families: topic-level settings that shape one topic, and broker-level settings that shape the cleaner machinery.

Topic settings decide when a log becomes eligible and how long special records live. Broker settings decide how much power the cleaner has. Here are the defaults as of recent Kafka releases. Always confirm against the documentation for your exact version.

Topic-level settings

SettingDefaultWhat it controls
cleanup.policydeleteChoose delete, compact, or compact,delete
min.cleanable.dirty.ratio0.5How dirty the log must be before cleaning starts
min.compaction.lag.ms0Minimum age a record must reach before it may be compacted
max.compaction.lag.msvery large (effectively never)Maximum time a record may wait before its log is forced to become eligible
delete.retention.ms86,400,000 (24 hours)How long tombstones are kept
segment.bytes1 GBMaximum size of one segment; smaller segments close and become cleanable sooner
segment.ms7 daysTime after which the active segment is rolled to a new one

Broker-level settings

SettingDefaultWhat it controls
log.cleaner.enabletrueTurns the cleaner machinery on or off
log.cleaner.threads1Number of worker threads
log.cleaner.dedupe.buffer.size128 MBTotal memory for offset maps, shared by threads
log.cleaner.io.buffer.size512 KBRead and write buffers used while copying
log.cleaner.io.buffer.load.factor0.9How full the offset map is allowed to get
log.cleaner.io.max.bytes.per.secondno limitSpeed limit for cleaner reads and writes
log.cleaner.backoff.ms15,000Sleep time when there is nothing to clean
log.cleaner.min.cleanable.ratio0.5Broker default for the dirty ratio
log.cleaner.min.compaction.lag.ms0Broker default for minimum compaction lag
log.cleaner.delete.retention.ms24 hoursBroker default for tombstone retention
i
Topic wins over broker

A topic-level value overrides the matching broker default. Set broker defaults for the cluster as a whole, then adjust individual topics that behave differently, such as a small hot configuration topic that should be compacted quickly.

Understanding the three timing settings together

Three settings work like a small traffic light system for when a record may be compacted.

  • Dirty ratio says: “Do not start unless enough new data has piled up.”
  • Minimum compaction lag says: “Do not touch records younger than this.” It protects consumers who need to see every change for a while, and it gives slow consumers time to catch up.
  • Maximum compaction lag says: “Even if the ratio is low, start no later than this.” It puts a ceiling on how long duplicates and deleted data may linger, though it is a target and not a strict deadline, because the cleaner threads may be busy.

Choosing segment size for compacted topics

The active segment is never cleaned. If segment.bytes is 1 GB and your topic gets only a few megabytes a day, the active segment could stay open for a week (the segment.ms default), and everything inside it stays uncompacted for that time. For compacted topics that need timely cleanup, use smaller segments such as 100 MB, or a shorter segment.ms such as one hour. Kafka itself uses 100 MB segments for its internal offsets topic for this reason. Do not go too small: thousands of tiny segments mean many open files and many small reads.

11Performance and Scalability

Compaction spends disk, memory and CPU in return for smaller logs and faster restarts. Here is how to keep the bill fair.

Where the cost comes from

Disk read

Reading the log

Each run reads the dirty region twice (once to build the map, once to copy) and reads the clean region once to filter it.

Disk write

Writing survivors

All records that stay are written again into new segments. This is called write amplification.

CPU

Hashing and compressing

Every key is hashed, and compressed batches are decompressed and recompressed.

Memory

Offset map and buffers

The dedupe buffer is allocated at startup and held for the life of the broker.

Page cache

Cache pollution

Reading old segments can push hot data out of the operating system cache, slowing down normal consumers.

Space

Temporary files

While a group of segments is rewritten, both old and new copies exist for a short time, so the disk needs some free headroom.

The dirty ratio trade-off

Lower ratio (for example 0.1)

  • Cleans more often, so duplicates disappear sooner
  • Uses less disk space on average
  • Consumers starting from zero read less data
  • Higher I/O and CPU load, more recopying

Higher ratio (for example 0.8)

  • Cleans less often, saving I/O and CPU
  • Logs hold much more stale data for longer
  • Late-start consumers read more duplicates
  • Deleted data lingers longer

Tuning steps that usually help

1

Measure before touching anything

Look at the cleaner metrics in Chapter 14. If the cleaner is idle and the dirty percentage is low, you do not need to tune anything.

2

Size the dedupe buffer

If cleaning takes many passes, raise log.cleaner.dedupe.buffer.size. This is often the biggest win.

3

Add threads if the disks are idle

Each extra thread can clean a different partition at the same time. But remember the buffer is split among threads, so raise the buffer when you add threads.

4

Throttle if consumers suffer

If regular traffic slows down during cleaning, set log.cleaner.io.max.bytes.per.second to a value your disks can share comfortably.

5

Right-size segments

Use segment sizes that close often enough for timely cleaning but not so small that file counts explode.

An illustrative capacity example

Suppose a topic keeps the latest profile for 20 million users, each about 1 KB. The compacted size is about 20 GB per replica, no matter how many updates happened in total. With a replication factor of 3, that is 60 GB across the cluster. A delete-policy topic holding a week of every update might need many times more. If each user updates twice a day, seven days means 280 million records, or about 280 GB per replica. Compaction stores the same current state in roughly one-fourteenth of the space, and a new consumer can load it in minutes instead of hours.

Scaling out

Compaction scales the same way Kafka scales: by adding partitions and brokers. More partitions split the keys, which means each partition has a smaller offset map to fill, and more cleaner threads across the cluster can work in parallel. The price is more files and more metadata, so choose partition counts carefully at design time, since changing the count later breaks the key-to-partition mapping that compaction depends on.

!
Key rule

Never add partitions to a compacted topic casually. New records for an existing key may start landing in a different partition than its older records. The two copies then live in different logs and can never replace each other, so old values stay forever.

12Reliability, Replication and Failure Recovery

Compaction rewrites files in a living system, so Kafka built it to survive crashes at any moment.

Independent cleaning, shared meaning

Leaders and followers each clean their own copy. Their files may look different in detail, for example with different segment boundaries, but the surviving records for each key match. Since offsets never change, a follower can keep fetching from a leader and a consumer can move to a new leader after failover without any confusion.

Crash safety during the swap

The swap step is designed so that a crash at any point leaves the log recoverable. New data is fully written to a .cleaned file first. Only when it is complete is it renamed to .swap. On broker startup, Kafka looks for leftovers. An unfinished .cleaned file is simply deleted because the original segments are still intact. A finished .swap file is used to complete the replacement. This means the log is never left with half of the old data and half of the new.

Everyday Analogy

It is like a mover who carries your belongings to a new apartment. Your old apartment stays fully furnished until the new one is completely set up and the keys are handed over. If the truck breaks down halfway, you still have your old home. Only after the new home is ready does the old one get emptied.

Common failure scenarios

ScenarioWhat happensWhat to do
Broker crashes mid-cleanStartup recovery removes or completes temporary files; the log is consistentNothing, but watch startup logs
Disk fills up during cleaningRewrite fails; the partition stays as it wasFree space, keep headroom of at least the size of one segment group per thread
Offset map too small for one segmentThat partition is marked uncleanable; others continueRaise buffer size or lower segment size
Corrupt record in a segmentThe cleaner reports an error and stops working on that partitionInvestigate, repair from a healthy replica, then restart
Cleaner falls behindDirty ratio climbs, disk grows, restarts get slowerTune buffer, threads, throttle, or add brokers
Consumer away past tombstone retentionMay keep deleted keys forever in its own stateRebuild consumer state from offset zero

Transactions and the safe cleaning limit

If a producer has an open transaction, its records are not yet decided as committed or aborted. The cleaner does not clean past the first offset of any open transaction, so it will never drop or keep a record based on an unfinished decision. Long-running transactions can therefore hold back compaction for their partition, which is one more reason to keep transactions short.

The uncleanable partition idea

A bad partition should not stop the whole cleaner. When a serious error happens for one partition, Kafka marks that partition as uncleanable, records it in metrics, and moves on to other partitions. This isolates the damage, but it also means a silent problem can hide unless you watch the metrics.

13Security and Data Privacy with Compaction

Compaction can help you forget data, but it does not forget on a schedule you can promise to a regulator.

Who can delete what

In Kafka, permission to write to a topic includes permission to write tombstones. That means anyone who can produce to a compacted topic can effectively delete any key in it. Treat write access to compacted topics like write access to a database table. Use ACLs so only the owning service can write, and separate read-only and read-write roles clearly.

Also protect topic configuration changes. Someone who can alter delete.retention.ms or the cleanup policy can change what gets erased and when. Limit config-altering rights to platform administrators.

The right-to-erasure problem

Privacy laws such as the GDPR include a right to have personal data erased. Teams often say: “We publish a tombstone, and compaction removes the data.” That is only partly true. Consider all the places the old value can still live:

  • In the active segment, which the cleaner cannot touch until it rolls.
  • In dirty segments waiting for the next run to trigger.
  • On every follower replica, each cleaned separately.
  • In records protected by the minimum compaction lag.
  • In backups, mirrored clusters, downstream topics, and consumer databases.

Making erasure faster

Setting

Set a maximum compaction lag

This forces the log to be cleaned within a target time even if the dirty ratio is low.

Setting

Roll segments regularly

A short segment.ms makes sure the active segment closes and becomes cleanable.

Setting

Lower tombstone retention carefully

Shorter delete.retention.ms removes tombstones sooner, but raises the risk for offline consumers.

Design

Encrypt per person, then throw away the key

Store personal data encrypted with a key per user. To erase, delete the key. The old ciphertext becomes unreadable everywhere at once, including in backups. This is often called crypto-shredding.

i
Best practice

Do not rely on compaction timing as your only erasure control. Combine tombstones, a tight maximum compaction lag, and per-user encryption keys. Document the expected deletion window for your legal team, and test it with real data.

Encryption and network safety

Compaction itself does not add encryption. Protect data in transit with TLS between clients and brokers and between brokers. Protect data at rest with disk-level or volume-level encryption. If a value contains sensitive fields, consider encrypting the field in the application before it is produced, because compaction copies records byte for byte and anyone with disk access sees the stored bytes.

14Monitoring, Logging and Metrics

A cleaner that quietly stops is worse than one that fails loudly. Watch these signals.

Key metrics

Metric (JMX name)MeaningWhat to look for
max-dirty-percentHighest dirty ratio across logsA steady climb means the cleaner is losing the race
time-since-last-run-msTime since a cleaner thread last finished workA very large value can mean the thread is stuck or dead
max-clean-time-secsLongest time one run tookSudden jumps hint at bigger logs or slower disks
cleaner-recopy-percentShare of cleaned bytes that were rewritten without changeA high value means most work is re-copying old data
max-buffer-utilization-percentHow full the offset map gotNear 100 percent means the buffer limits progress
uncleanable-partitions-countPartitions the cleaner gave up onAny value above zero deserves attention
uncleanable-bytesTotal size of those partitionsTracks how much data is stuck

Logs

Kafka writes cleaner activity to a dedicated file, usually named log-cleaner.log, next to the broker logs. It records which partition was cleaned, how many bytes were read and written, how many records were retained, the compression ratio, and the duration. It is the first place to look when someone asks “why is this topic still huge?”

Simple alerts worth setting

  • Alert if uncleanable-partitions-count stays above zero for more than a few minutes.
  • Alert if time-since-last-run-ms grows well beyond your backoff time on busy clusters.
  • Warn if max-dirty-percent stays high for many hours.
  • Watch free disk space on every data directory, since compaction needs working room.
  • Track the size of your key compacted topics over time; a slow leak often means keys are never deleted.

15Advanced Internals: Batches, Headers and the Delete Horizon

Now that the big picture is clear, let us look at the fine print that decides how the cleaner treats each byte.

The record batch, the real unit on disk

Earlier we said Kafka groups records into batches. Each batch carries a small header, and the cleaner reads and rewrites that header with care. The header holds the base offset of the batch, its length, the leader epoch that wrote it, a checksum, a set of attribute bits, the timestamps, the producer identity, the first sequence number, and the record count. Inside the batch, every record stores a small offset delta relative to the base offset. That design is why offsets stay stable: when the cleaner drops a record, it does not renumber anything. It simply omits that record, and the deltas of the remaining records stay exactly as they were.

Header field

Base offset and deltas

Give every record its permanent address. Removing a record leaves a hole, never a shift.

Header field

Attribute bits

Tell whether the batch is compressed, transactional, or a control batch. They also carry a flag related to the delete horizon.

Header field

Producer identity

Producer id, epoch and sequence numbers let brokers detect duplicate sends. The cleaner preserves them so idempotence keeps working.

Header field

Timestamps

The first and maximum timestamps of the batch help retention and compaction lag decisions.

The delete horizon

Earlier we said newer Kafka versions start the tombstone countdown when the cleaner first sees it. The cleaner remembers this moment by writing a delete horizon into the batch that holds the tombstone. A flag in the batch header says that the timestamp field now carries the horizon, meaning the time after which the tombstone may be dropped. On later runs, the cleaner just compares the horizon to the current time. This is more reliable than using file modification times, because file times can change when segments are copied or rewritten for unrelated reasons.

Control records and transaction markers

Transactions add special control records that mark a transaction as committed or aborted. Readers never see them, but they exist in the log. The cleaner treats them in a careful way:

  • Records from aborted transactions are removed, since no committed reader should ever see them.
  • Commit and abort markers are kept for a while, following the same delete-retention idea used for tombstones, so consumers that are still reading through the region can interpret earlier batches correctly.
  • Cleaning never advances past an offset that belongs to an open transaction. The lowest such offset is called the first unstable offset, and it sits at or above the last stable offset that consumers in read-committed mode are allowed to see.
Everyday Analogy

Think of a school that keeps attendance sheets. A sheet with a “pending review” stamp cannot be archived or trimmed until the teacher marks it approved or rejected. The cleaner respects the same rule: nothing that is still waiting for a final decision is touched. Only after the stamp changes can the sheet be filed or thrown away.

Other files that survive cleaning

A partition directory holds more than segments. It also has a leader epoch checkpoint that tracks which leader wrote which offset range, and producer state snapshots that remember the latest sequence number of each producer. Compaction does not touch these files. That is why keeping the last batch of each producer, even when empty, is important: it allows the broker to rebuild producer state after a restart by scanning only recent batches.

Why the cleaner works per batch and not per record

Kafka compresses at the batch level, and the checksum covers the whole batch. Editing a record inside a batch means rebuilding the batch. So the cleaner reads a batch, decides which of its records survive, and writes a new batch containing only those records. If all records survive, it can copy the batch bytes as they are. This shortcut is one reason cleaning a log full of unique keys is cheaper per byte than cleaning a log full of duplicates.

i
Inspection tip

Kafka ships with a log dump tool that prints the batches and records in a segment file, including offsets, timestamps, and whether a record is a tombstone. Running it on a segment before and after cleaning is the best way to see offset gaps and delete horizons with your own eyes on a test cluster.

Large messages and the cleaner buffers

The read and write buffers of the cleaner start at the size set by log.cleaner.io.buffer.size. If a single batch is bigger than the buffer, the cleaner grows the buffer up to the largest allowed message size so it can still process that batch. This means a few unusually large records can temporarily increase memory use. Keep an eye on maximum message size settings for compacted topics, and avoid stuffing large blobs into values when a reference to external storage would do.

16Compaction Compared with Other Ways to Keep Latest State

Compaction is one of several tools. Knowing its neighbors helps you choose well.

ApproachStrengthWeaknessBest when
Compacted Kafka topicOrdered stream and current state in one place; fast bootstrap; replicatedNo query by anything but offset; cleaning is approximateYou need both a change feed and a snapshot
Delete-policy topic plus external databaseRich queries and updates in the databaseTwo systems to keep consistent; extra write pathYou need ad hoc lookups by many fields
Database with its own change logStrong transactions and constraintsDownstream systems must poll or be fed by another toolThe database is the system of record
Periodic snapshot filesSimple, and the size is exactly the live dataSnapshot is only as fresh as its schedule; no built-in change feedSlow-changing reference data
Stream processing state storeVery fast local lookupsNeeds backup; restoration takes timeJoins and aggregations at high speed

Compaction is not a database

A compacted topic answers exactly one question well: “What is the latest record for each key, and what changed since offset X?” It cannot answer “Which users live in Pune?” without reading everything. If your reads look like queries, put a query engine or a database next to the topic and feed it from the topic. Many good architectures use the compacted topic as the source of truth for change and the database as the read-optimized view.

Compaction and tiered storage

Kafka’s tiered storage feature moves older closed segments to cheaper remote storage such as an object store. Support for compacted topics in that feature has been limited or unavailable in releases so far. Check the documentation for your version before planning to combine them, and remember that a compacted topic already stays small when the number of live keys is modest.

Compaction and Kafka Streams tables

A table in Kafka Streams is stored twice: once in a fast local store for lookups, and once in a compacted changelog topic for safety. Compaction is the safety net, not the working copy. This split shows a general lesson: use the compacted topic for durability and replay, and keep a local or external structure for fast reads.

Use the compacted topic as the durable memory, and something else as the fast lookup.

17How Consumers and Stream Apps Use Compacted Topics

The reader side of compaction has its own patterns. Get them right and your services start fast and stay correct.

The bootstrap-then-follow pattern

Most consumers of compacted topics do two jobs in order. First they bootstrap: read from the beginning until they reach the end of the log as it was when they started. This gives them the current state. Then they follow: keep reading new records and apply each as an update or a delete.

flowchart LR
  A["Service starts with empty local state"] --> B["Ask broker for end offsets of each partition"]
  B --> C["Read from offset zero, applying each record"]
  C --> D{"Reached the recorded end offsets?"}
  D -->|"No"| C
  D -->|"Yes"| E["Mark service as ready"]
  E --> F["Keep reading, apply updates and tombstones live"]
FIG 5 — Bootstrap until the recorded end offsets, then follow live changes.

The end offsets are noted at the start so the consumer has a clear finish line. Without it, a busy topic could keep the consumer “bootstrapping” forever. Because compaction leaves gaps, “reached the end” must be tested by comparing the consumer’s position to the end offset, not by counting records.

Applying records correctly

  • Upsert on a normal record. Put the value into the local map under the key, replacing any older value.
  • Remove on a tombstone. A null value means remove the key. Do not store null as a value.
  • Be idempotent. Reading the same record twice, for example after a restart, must leave the state unchanged. Upserts and removes are naturally idempotent, which is one of the beauties of this pattern.
  • Use partition-aware state. Keys of one partition never overlap with another, so each partition can be loaded by a different worker in parallel.

Kafka Streams restoration

When a Kafka Streams instance starts on a fresh machine, it restores each local store by reading the matching changelog topic from the beginning. The time this takes depends on the compacted size of the topic, not on its lifetime write volume, and that is compaction at work. To cut restart delays even more, teams enable standby replicas. A standby is another instance that continuously applies the changelog to a warm copy, so failover only needs to catch up on the last few records.

Global tables and small reference data

Some data, such as country codes or product categories, is small and needed by every instance. A global table loads every partition of a compacted topic into each instance. Because compaction keeps the topic as small as the number of live keys, this stays cheap even after years of updates.

!
Common trap

Bootstrapping treats old data as truth, so schema changes matter. A quiet key can still hold a record written years ago in an old format. Your consumer must be able to read every format that ever lived in the topic, not just the newest one.

Deciding when a consumer is caught up

For read-committed consumers, the end of the readable log is the last stable offset. For plain consumers, it is the high watermark, which is the highest offset copied to all in-sync replicas. Both are reported by the broker. A service that says “ready” too early would answer requests using incomplete state, which is a classic subtle bug in cache-loading services.

18Capacity Planning Walkthrough: A Worked Example

Numbers make ideas real. Let us size a compacted topic step by step for an imaginary but realistic service.

Imagine an online store that keeps a compacted topic called product-catalog. It expects 50 million products, each with a value of about 2 KB, spread across 12 partitions, and a replication factor of 3. Updates arrive at a steady pace, and the team uses the default dirty ratio of 0.5.

Step 1: the live data

QuantityCalculationResult
Live data per replica50 million × 2 KBabout 100 GB
Live data, three replicas100 GB × 3about 300 GB
Keys per partition50 million ÷ 12about 4.17 million

Step 2: worst-case log size before cleaning

At a dirty ratio of 0.5, the cleaner may wait until the dirty region equals the clean region. In the worst case, every dirty record replaces a clean one, so the log can reach twice its compacted size. For our example that is 200 GB per replica, or 600 GB for all three copies. Add the active segment and some slack for growth, plus 20 percent free space for temporary cleaning files, and you should plan roughly 720 GB of raw capacity for this topic alone.

Step 3: the offset map

In the worst case, one partition’s dirty region contains every one of its 4.17 million unique keys. At 24 bytes per key, that is about 100 MB. Dividing by the 0.9 load factor gives about 111 MB per thread. The default 128 MB buffer with a single thread would pass barely. If the team adds a second thread, each thread gets only 64 MB, and cleaning would need two runs per partition. A sensible choice is to raise the dedupe buffer to 512 MB with two threads, giving each 256 MB with a comfortable margin.

Step 4: the cleaning workload

Each run re-reads the whole partition up to the map’s limit. A partition holds about 8.3 GB of compacted data (100 GB ÷ 12) and up to about 16.7 GB when dirty. A run may therefore read around 17 GB and write about 8 GB. With a throttle of 50 MB per second, that takes several minutes. Twelve partitions spread across brokers and threads finish in parallel. This is a manageable load for modern disks, but it should be measured, not guessed.

Step 5: consumer startup time

A new consumer reads about 100 GB to bootstrap. At a sustained read rate of 200 MB per second, that takes roughly eight to nine minutes. With twelve partitions read by twelve parallel workers, each reads only around 8 GB, so bootstrap drops to about one minute. Lowering the dirty ratio would reduce it a bit further by trimming duplicates in the log.

i
Planning rule

Plan disk for two times the compacted size, buffer for keys per partition times 24 divided by 0.9, and headroom for temporary files. Then watch real metrics for a week and adjust.

19Troubleshooting Playbook

When something looks wrong, walk through these checks in order. Most compaction problems fall into a handful of buckets.

Symptom: the topic is not shrinking

1

Confirm the policy

Check that the topic really has the compact policy and that the broker has the cleaner enabled. A topic that quietly uses the delete policy will never be compacted.

2

Look at the segments

List the segment files. If there is only one huge active segment, nothing is cleanable yet. Reduce segment size or roll time.

3

Check the dirty ratio and lags

A high minimum compaction lag or a high dirty ratio threshold can keep the log ineligible for long stretches.

4

Read the cleaner log

Look in the cleaner log file for errors about the partition. Errors about the offset map or corrupt records point to buffer size or data problems.

5

Check the metrics

An uncleanable partition count above zero, or a stale last-run time, confirms that the cleaner is stuck rather than idle.

6

Think about the keys

If keys are unique per record, or spread over partitions after a partition count change, the log will never shrink. This is a design issue, not a cleaner issue.

Other symptoms and likely causes

SymptomLikely causeFirst action
Deleted keys reappear in a consumerConsumer was offline longer than tombstone retentionRebuild its state from the beginning; consider longer retention
Consumers see old values for a keyDirty region not cleaned yet; this is allowedUse last-write-wins logic in the consumer
Disk usage spikes during cleaningTemporary files for rewritten segment groupsAdd headroom; use smaller segments
Normal traffic slows during cleaningCleaner competes for disk and page cacheSet the I/O throttle; separate log directories if possible
Cleaning takes many passesOffset map too small for unique keysRaise dedupe buffer size
Startup of a service is slowTopic is large or full of duplicatesLower dirty ratio, add partitions in a new topic, add standby replicas
Cleaner seems dead after an errorPartition marked uncleanable or thread stuckRead the log, fix the cause, and restart the broker if the thread does not recover
!
Safety first

Never delete segment files by hand on a live broker to “help” compaction. The broker tracks files in memory and in checkpoints, and manual changes can cause data loss or a failed startup. If a partition is damaged, repair it by re-replicating from a healthy replica.

20Testing and Evolving Compacted Topics

Compaction behaves differently from ordinary retention. Test it on purpose, and plan for change from day one.

How to test compaction in a staging cluster

Waiting days for default settings is not a practical test. Instead, create a test topic with aggressive settings: a very small segment size, a tiny dirty ratio, a zero compaction lag, and a short tombstone retention. Then follow this routine:

1

Write repeated updates

Produce many updates for a small set of keys, so segments close and duplicates pile up.

2

Write a few tombstones

Delete some keys and note which ones.

3

Wait for cleaning

Give the cleaner a few backoff cycles, then check the segment files and the cleaner log.

4

Read from the beginning

Run a fresh consumer from offset zero and compare its rebuilt state to what you expect. Deleted keys should be absent and live keys should have their latest values.

5

Test the slow consumer

Pause a consumer, let cleaning run and the tombstone retention pass, then resume it and check what it does. This exposes ghost keys before production does.

Schema evolution with long-lived records

In a delete-policy topic, records age out and old formats disappear on their own. In a compacted topic, a quiet key can keep a record for years. So every version of your value schema that has ever been produced may still exist in the log. Adopt compatibility rules that let a new consumer read every past version, and a new producer avoid breaking old readers. In schema registry terms, the transitive backward-compatible modes suit compacted topics well.

Keys are part of the contract

The cleaner compares keys as raw bytes. Changing how you serialize the key, such as adding a field to a structured key or switching from text to binary, produces different bytes for the same logical entity. The old and new records then look like different keys, and old copies remain forever. Treat the key format as frozen once a compacted topic is in production.

ANTI-PATTERN 01Avoid
What people do

Add an optional field to a structured key schema because “it is backward compatible.”

Why it hurts

Even a compatible schema change alters the serialized bytes of the key. The cleaner sees new keys, so every entity now has two live records.

Do this instead

Keep keys simple and stable, such as a single identifier. Put changing details in the value.

Migrating to a new topic

When you must change partitions, keys, or settings that cannot be changed in place, migrate. Create the new topic, copy the latest records from the old one with a small program or a replication tool, switch producers, let consumers catch up, then switch consumers and retire the old topic. Because the old topic is compacted, the copy only needs to transfer the live data, not the whole history, which makes migration much cheaper than with a delete-policy topic.

21Design Patterns and Anti-patterns

A compacted topic is a tool with a shape. Use it where the shape fits, and avoid it where it does not.

Patterns that work well

Topic as a table

Treat each key as a row and each record as an update to that row. A consumer that reads the whole topic can build the entire table in memory or in a local database. Kafka Streams calls this a KTable. It is the most natural use of compaction.

State backup through a changelog

A stream processing app keeps its running totals in a local store for speed. Every change to the store is also written to a compacted topic. If the app moves to another machine, it replays the topic and gets its state back. Because the topic is compacted, the replay reads only the latest value per key, not every historical update.

Configuration and reference data distribution

Feature flags, pricing tables, and product catalogs can be pushed as keyed records. Every service reads the topic on startup to get the complete current set, then keeps listening for changes.

Change data capture keyed by primary key

A tool reads a database’s change log and publishes one record per row change, keyed by the row’s primary key. A compacted topic then holds the latest version of every row, which works like a live copy of the table for other systems to bootstrap from.

Entity snapshots beside an event log

Keep the full event history in a delete-policy topic for a limited time, and publish periodic entity snapshots to a compacted topic. New services can start from the snapshot and only replay recent events.

Anti-patterns to avoid

ANTI-PATTERN 02Avoid
What people do

Use a random unique ID as the key for every record, such as a new UUID for each event, and still enable compaction.

Why it hurts

Every record has a different key, so nothing is ever replaced. The log never shrinks, and the offset map fills quickly with useless entries.

Do this instead

Use compaction only when the key names a real entity that gets updated. For pure event streams, use the delete policy.

ANTI-PATTERN 03Avoid
What people do

Treat a compacted topic like a message queue where every message must be seen exactly once by a slow consumer.

Why it hurts

Compaction may remove intermediate updates for a key before a slow consumer reads them. The consumer sees the final state, not every step.

Do this instead

If every step matters, use a delete-policy topic, or set a minimum compaction lag long enough for your slowest consumer, and monitor consumer lag.

ANTI-PATTERN 04Avoid
What people do

Change the number of partitions or the key format of a live compacted topic.

Why it hurts

The same logical key may now live in two partitions, or under two different byte patterns. Old copies are never replaced and the topic silently grows and gives wrong snapshots.

Do this instead

Create a new topic with the new layout, copy the data across, switch producers and consumers, and retire the old topic.

ANTI-PATTERN 05Avoid
What people do

Expect a tombstone to remove data instantly and use it as the only privacy control.

Why it hurts

The old value can survive in the active segment, in replicas, in backups, and downstream. Legal deadlines can be missed.

Do this instead

Combine tombstones with compaction lag limits, rolling segments, and per-user encryption keys.

ANTI-PATTERN 06Avoid
What people do

Leave the default 1 GB segment size and one-week roll time on a small, low-traffic compacted topic and wonder why it is never cleaned.

Why it hurts

The active segment absorbs all the data and is exempt from cleaning, so duplicates pile up until the segment rolls.

Do this instead

Set a small segment.bytes or a short segment.ms on compacted topics that need timely cleanup.

22Real-World Usage: Where Compaction Runs in Production

You have probably used compaction today without knowing it, because Kafka’s own brain depends on it.

Inside Kafka itself

The consumer offsets topic

When a consumer group reads a topic, Kafka needs to remember how far each member got. It stores this in an internal compacted topic called __consumer_offsets, with 50 partitions by default. The key is the combination of group, topic and partition. The value is the latest committed offset. When a broker becomes the coordinator for a group, it reads the relevant partition and rebuilds its memory of the latest commits. If the topic were not compacted, that recovery would need to replay years of commits.

The transaction state topic

Kafka tracks the state of every transactional producer in another internal compacted topic, __transaction_state. Only the latest state per producer matters, so compaction fits perfectly.

Kafka ecosystem tools

Kafka Streams

Changelog topics

Stateful stream apps back up their local stores into compacted changelog topics. Windowed stores use compact plus delete so old windows are eventually forgotten.

Kafka Connect

Internal storage topics

Connect stores connector configuration, source offsets, and status in compacted topics, so a restarted worker can always find the latest settings.

Schema Registry

The schemas topic

Confluent’s Schema Registry keeps all registered schemas in a single-partition compacted topic and reloads them on startup.

Debezium

Change data capture

Red Hat’s Debezium publishes database row changes keyed by primary key. By default it also emits a tombstone after a delete so compacted topics can drop the row.

Industry background

Kafka was created at LinkedIn, and its designers described compaction as a way to treat a log as a replicated, ordered copy of database state. Jay Kreps, one of the original authors, wrote an influential essay called “The Log” which explains why a compacted change log is a natural way to keep systems in sync. Apache Samza, another LinkedIn project, uses compacted changelog topics to make stream processing state recoverable. Today, the same idea appears anywhere that companies mirror database tables into streams: every consumer can bootstrap from the compacted topic, then follow live changes.

i
Version note

Kafka’s newer metadata system, KRaft, does not use log compaction for its cluster metadata log. It uses periodic snapshots instead. Compaction remains central for user topics and the internal topics listed above.

Pros and cons at a glance

Pros

  • Log size follows the number of live keys, not total writes
  • New consumers get a full current-state snapshot fast
  • No offsets change, so consumers stay compatible
  • Works quietly in the background on every replica
  • Enables state recovery for stream apps and services

Cons

  • Uses extra disk, CPU and memory for cleaning
  • Cleanup timing is approximate, not instant
  • Requires a key on every record
  • History is lost by design
  • Tiered storage support is limited; check your version’s documentation

23Best Practices and Common Mistakes

A short field guide from the lessons teams learn the hard way.

Best practices

Keys

Design stable keys

Pick keys that name a real entity and never change format. Use a consistent serializer so equal keys always produce equal bytes.

Partitions

Plan partition count early

Choose enough partitions at creation for future growth. Avoid changing it later.

Segments

Match segment size to traffic

Low-traffic compacted topics need smaller segments or shorter roll times to be cleaned promptly.

Memory

Size the dedupe buffer

Estimate unique keys in the dirty region and give each thread enough buffer to cover it.

Deletes

Choose tombstone retention deliberately

Set it longer than the longest expected consumer outage. Rebuild consumers that are gone longer.

Ops

Monitor uncleanable partitions

Alert on any non-zero count, and keep disk headroom for temporary files.

Consumers

Never assume dense offsets

Write consumers to handle gaps and duplicates near the head of the log.

Privacy

Layer your erasure controls

Use tombstones, compaction lag limits, and per-user encryption together.

Common mistakes

  • Forgetting keys. Producers that omit keys are rejected by compacted topics, and code that worked on a normal topic suddenly fails.
  • Using empty strings as deletes. Only a true null value is a tombstone. An empty value is just another update.
  • Expecting instant cleanup. Cleaning waits for the dirty ratio, the compaction lag, and the active segment to roll.
  • Tuning the wrong knob. Adding threads does not help if the buffer is too small for a single segment.
  • Ignoring the recopy cost. Very large logs with frequent cleaning can spend most of their effort recopying unchanged data.
  • Counting records by offset. Gaps make offset arithmetic meaningless.
  • Combining compact with a short retention by accident. Latest values then start disappearing.
  • Restarting consumers with stale local state. After long downtime, replay from the beginning.

24Frequently Asked Questions

Quick answers to the questions people ask most often.

Q1Does compaction change my offsets?
No. Every record that survives keeps the exact offset it was given when written. Only the numbers of removed records go missing, which creates gaps.
Q2Can I read a compacted topic like a normal topic?
Yes. Consumers use the same API. The only difference is that older parts of the log may lack replaced records and offsets may have gaps.
Q3How do I delete a key?
Produce a record with that key and a null value. After the cleaner runs and the tombstone retention time passes, both the old values and the tombstone disappear.
Q4Why is my compacted topic still huge?
Common causes: many unique keys, a huge active segment that has not rolled, a dirty ratio that has not been reached, an uncleanable partition, or a cleaner buffer too small to make progress. Check the cleaner metrics and log-cleaner.log.
Q5Does compaction run on the leader only?
No. Every broker cleans its own replicas independently. Their files can differ in layout, but they keep the same newest value for each key.
Q6Is the active segment ever compacted?
Not while it is active. Once it rolls and becomes a closed segment, the cleaner can process it.
Q7What is the difference between minimum and maximum compaction lag?
The minimum protects young records from being compacted too early. The maximum makes sure that a log is forced to become eligible after a certain time even if it is not dirty enough.
Q8Can I use compaction together with time-based retention?
Yes, by setting the policy to compact,delete. Records are compacted, and anything older than the retention time is also removed, even if it is the latest value for its key.
Q9Why did my consumer miss a delete?
Most likely it was offline longer than the tombstone retention time. The tombstone was removed before it could see it. Rebuild its state from the beginning of the topic.
Q10How much memory does the cleaner need?
About 24 bytes per unique key in the dirty region being processed, divided by the load factor of 0.9. The default buffer of 128 MB handles about five million unique keys per run.
Q11Does compaction remove duplicates of the same value?
Compaction is based on keys, not values. If two records have the same key, only the newest survives. Two records with different keys and the same value both stay.
Q12Is it safe to change cleanup.policy on an existing topic?
You can, but think carefully. Moving from delete to compact will eventually remove history, and adding delete to compact can remove latest values that are older than the retention. Test on a copy first.
Q13Does KRaft use log compaction?
Not for the cluster metadata log. It uses snapshots. Compaction is still used for user topics and internal topics such as consumer offsets.

25Summary and Key Takeaways

Everything above, in a form you can remember.

Log compaction turns a Kafka topic from a history book into a living table. A small group of cleaner threads inside each broker scan the older, closed part of every compacted log. They build a compact memory of the newest offset per key, then copy the log while dropping everything that has been replaced. Offsets never move, order never changes, and consumers keep working as usual. Deleted keys travel as tombstones that stay for a grace period so slow readers can learn of them, then quietly vanish.

Key Takeaways

  • Compaction keeps the latest value per key. It removes data because it was replaced, not because it is old.
  • Offsets and order are permanent. Gaps appear, but no surviving record ever moves.
  • The cleaner works in two phases. It builds an offset map from the dirty region, then re-copies the log from the start, keeping only the newest record for each key.
  • The offset map sets the pace. Each unique key costs 24 bytes, so the buffer size decides how much a single run can cover.
  • Tombstones delete by writing. A null value marks a key as gone, and it is kept for the delete retention time before being dropped.
  • Timing is approximate. The dirty ratio, compaction lag settings, and segment rolling decide when cleaning happens, and the active segment is never cleaned.
  • Design and monitor it deliberately. Choose stable keys, keep partition counts fixed, size the buffer, watch for uncleanable partitions, and never rely on compaction alone for privacy.