High Watermark and Log End Offset in Kafka
Two small numbers decide what a consumer is allowed to read, when a producer hears "success", and whether your data survives a broker crash. This guide follows both numbers from the first message to the last failover.
Picture a payments platform at peak hour. A customer taps Pay, and the order service writes a small message into Kafka saying the money has moved. Somewhere else, a receipt service is waiting to read that message and email the customer. The question sounds tiny, yet it hides a whole distributed system: at exactly which moment is it safe for the receipt service to see that message?
If Kafka lets the reader see the message too early, it might exist on only one machine. If that machine dies, a receipt has gone out for a payment that Kafka has now forgotten. If Kafka waits too long, every application built on top of it feels slow. Kafka solves this balancing act with two small numbers that every copy of a partition keeps: the Log End Offset and the High Watermark.
This guide explains both from scratch. You will learn what each number means, who updates it, how the two move as messages flow, how they shape producer acknowledgements and consumer reads, and what happens when brokers crash or restart. Every new word is explained the first time it appears, and each big idea comes with an everyday comparison and a real-world example.
KKafka in Five Minutes
Before we talk about two special numbers, we need a shared vocabulary. Here is the smallest useful picture of Kafka.
Apache Kafka is a system for moving streams of events between programs. An event (Kafka usually calls it a message or a record) is a small fact: “order 4471 was paid”, “driver 88 moved to a new street”, “user clicked Buy”. Programs that create events are called producers. Programs that read them are called consumers. The machines in the middle that store the events are called brokers, and a group of brokers working together is a Kafka cluster.
Events are grouped into topics. A topic is like a named channel or folder: one for payments, one for clicks, one for driver locations. A single machine cannot store or serve a giant stream alone, so each topic is cut into partitions. A partition is an ordered, append-only list of messages. “Append-only” means new messages are added at the end and old ones are never edited in place.
Offsets: the line numbers of a partition
Every message in a partition receives a number called its offset. The first message gets offset 0, the next gets 1, then 2, and so on. An offset is not a timestamp and not a global ID. It is a position inside one partition. Partition 0 and partition 1 of the same topic each have their own offset 0, and they never share a counter.
Think of a notebook with numbered lines. You only ever write on the next empty line, and you never erase or squeeze text between lines. The line number of any sentence never changes. A partition is that notebook, and the offset is the line number.
Producer
A program that sends messages to a topic. It decides which partition each message goes to, often by hashing a key.
Consumer
A program that reads messages from a partition, one offset after another, and remembers how far it has read.
Broker
One Kafka server. It stores partitions on disk and answers produce and fetch requests over the network.
Topic
A named stream, such as payments or page-views. A topic is split into partitions so the work can be shared.
Partition
One ordered log of messages. Order is guaranteed inside a partition, not across the whole topic.
Offset
The sequence number of a message inside its partition. It starts at 0 and only grows.
The log on disk
On disk, a partition is stored as a log: a folder holding a series of files called segments. Kafka writes only to the end of the newest segment. Sequential writes are very fast because disks and operating system caches are built for them. Old segments are deleted or compacted later according to retention rules, such as “keep seven days of data”.
Both the Log End Offset and the High Watermark are simply offsets, meaning line numbers in this notebook. The whole story is about which line number means what, and who is allowed to trust it.
RReplication: Keeping Copies Safe
One copy of your data on one machine is a single point of failure. Kafka keeps several copies of every partition on different brokers.
Each copy of a partition is called a replica. The number of copies is the replication factor. With a replication factor of 3, every partition lives on three different brokers, and the cluster can lose a machine without losing the data. Many production clusters use a factor of 3 because it balances safety against storage cost.
Among the replicas of a partition, exactly one is elected the leader. All writes from producers go to the leader. By default, reads from consumers also go to the leader. The other replicas are followers. A follower has one job: copy the leader’s log, in order, as quickly as it can. Followers do this by sending the leader ordinary fetch requests, the same kind of request a consumer uses.
A teacher writes lines on a board, and each student copies them into a personal notebook. The teacher is the leader and the students are followers. Some students write fast and some write slowly, so at any moment the notebooks are at different line numbers. We will use this classroom for the rest of the guide.
The In-Sync Replicas (ISR)
Not every follower keeps up. A broker can be slow, overloaded, or cut off by a network problem. Kafka therefore tracks a list called the In-Sync Replicas, or ISR. It contains the leader plus every follower that is currently caught up closely enough. A follower that lags for too long is removed from the ISR, and it is added back once it catches up again.
The ISR is the group of “students in good standing”. It matters enormously for our two numbers, because the High Watermark is computed from the ISR only, not from every replica that exists on paper.
| Term | Plain meaning | Example |
|---|---|---|
| Replica | One copy of a partition’s log on one broker | Partition 0 has copies on Broker 1, 2 and 3 |
| Leader | The replica that accepts writes and serves reads | Broker 1 holds the leader replica |
| Follower | A replica that copies the leader | Brokers 2 and 3 hold followers |
| Replication factor | How many replicas each partition has | 3 |
| ISR | The leader plus followers that are caught up | Brokers 1, 2 and 3, or just 1 and 2 if Broker 3 lags |
The ISR is not the same as “all replicas”. It is a moving list that shrinks and grows as brokers slow down or recover. Keep this in mind, because it decides how much protection a committed message really has at a given moment.
LLog End Offset (LEO)
Every replica, leader and followers alike, keeps its own private Log End Offset. It answers one question: how much have I written so far?
The Log End Offset (LEO) of a replica is the offset that the next new message will receive in that replica’s log. Another way to say it: LEO equals the offset of the last message plus one. If a replica holds messages at offsets 0 through 9, its LEO is 10. If its log is empty, its LEO is 0.
Back in the classroom, each student’s LEO is the number of the next blank line in their own notebook. A student who has filled lines 0 to 9 will write the next sentence on line 10. The blank line is the LEO, not the last filled line.
LEO points at an empty slot, not at the newest message. If someone tells you a partition’s LEO is 10, the newest message sits at offset 9. Most beginner mistakes with these two numbers come from forgetting this “plus one”.
Every replica has its own LEO
This is the first big idea. There is no single “LEO of the partition”. There is one LEO per replica. The leader’s LEO moves up when a producer’s batch is appended to the leader’s log. A follower’s LEO moves up when the follower appends data it fetched from the leader. Because followers copy after the leader writes, follower LEOs are usually equal to or smaller than the leader’s.
offsets 0 to 9 written
fully caught up
three messages behind
The gap between the leader’s LEO and a follower’s LEO is called replica lag. In the example above, Broker 3 has a lag of 3 messages. A small lag that comes and goes is normal, since copying always trails writing by a moment. A lag that keeps growing is a warning sign that a broker is struggling.
When can a LEO change?
A LEO grows whenever new data is appended. Under normal conditions it never shrinks. There is one exception that we will study in detail later: after a crash and a leader change, a follower may discover that its log contains messages the new leader never had. It must then cut those messages off, and its LEO moves backwards. This deliberate rewinding is called truncation.
LEO tells you what a replica has written. It says nothing about whether that data is safe. Safety belongs to the next number.
HHigh Watermark (HW)
If LEO says how much has been written, the High Watermark says how much is safe. It is the line that separates “written” from “committed”.
The High Watermark (HW) of a partition is the offset up to which every in-sync replica has a copy of the data. Precisely, the HW equals the smallest LEO among all replicas in the ISR. Messages with an offset below the HW are called committed. Messages at or above the HW exist on the leader but are still uncommitted.
Take the earlier numbers: the leader has LEO 10, one follower has LEO 10, and another follower has LEO 7. The smallest of the three is 7, so the HW is 7. Offsets 0 to 6 are committed. Offsets 7, 8 and 9 sit on the leader (and one follower) but are not yet committed, because the slowest in-sync replica has not copied them.
The class may only “officially move past” a line once every student in good standing has copied it. The slowest student sets the pace of the official record. If the teacher has written ten lines but the slowest student has copied seven, the official record ends at line seven, even though the board shows more.
The two promises of the High Watermark
Consumers see only committed data
A normal consumer can read up to, but not including, the HW. Uncommitted messages stay hidden until they become committed.
Committed data survives failures
Every in-sync replica holds every committed message, so as long as one in-sync replica survives, the message is not lost.
Why not let consumers read everything?
Imagine a consumer could read the uncommitted tail of the leader’s log. Then the leader crashes before any follower copies those messages. A follower becomes the new leader, and it has never seen them. The consumer has now processed events that, from Kafka’s point of view, never happened. It could have charged a card, sent an email, or updated a balance based on a ghost. Worse, its saved position could point past the end of the new leader’s log.
By hiding everything above the HW, Kafka guarantees that what consumers see is a stable history. Any message a consumer has been shown will still be there after a leader change. This guarantee is the entire reason the High Watermark exists.
A watermark is the mark left on a wall by a flood or on a dam by the water level. The High Watermark is the highest offset that is “under water”, meaning fully replicated. Everything above the line is still dry, and still at risk.
SLEO and HW Side by Side
Now that each number has a definition, let us put them in one picture and one table, so the difference sticks.
flowchart LR P["Producer"] --> L["Leader replica on Broker 1
LEO = 10"] L -->|"followers fetch"| F1["Follower on Broker 2
LEO = 10"] L -->|"followers fetch"| F2["Follower on Broker 3
LEO = 7"] L --> HW["High Watermark = smallest ISR LEO = 7"] HW --> C["Consumer can read offsets 0 to 6"]
Read the figure from left to right. The producer writes to the leader, whose LEO becomes 10. Two followers copy from the leader, and their LEOs are 10 and 7. The leader looks at all three LEOs, picks the smallest, and calls that number the High Watermark. Consumers are then allowed to read offsets 0 through 6.
| Question | Log End Offset (LEO) | High Watermark (HW) |
|---|---|---|
| What does it mark? | The next offset to be written in one replica’s log | The first offset that is not yet committed |
| How many exist? | One per replica | One authoritative value on the leader, copied by followers |
| What moves it? | Appending data to that replica’s log | Followers catching up, as seen by the leader |
| Who cares? | Replication logic and lag monitoring | Consumers, producers using acks=all, and recovery |
| Can it move backwards? | Yes, after truncation on a follower | Not on a healthy leader |
| Simple meaning | How much is written | How much is safe |
The three zones of a leader’s log
At any moment the leader’s log can be divided into three zones, using the two numbers as borders. Understanding these zones makes almost every Kafka replication behaviour easy to predict.
Committed
Every in-sync replica has it. Consumers may read it. It survives the loss of any single replica that is not the last one standing.
Written but uncommitted
The leader has it and some followers may too, but not all of them. Hidden from consumers. At risk if the leader fails right now.
Not written yet
Empty space. The next message a producer sends will land exactly at the LEO offset.
First, on any replica, HW is less than or equal to LEO, since you cannot commit what you have not written. Second, the leader’s HW is less than or equal to the LEO of every replica in the ISR, since HW is defined as their minimum.
JA Message’s Journey from Producer to Consumer
Let us follow one message through its whole life and watch both numbers move at each step.
We will use a partition with three replicas, all in the ISR. At the start, every replica has LEO 10 and the HW is 10, which means offsets 0 to 9 are committed and nothing is in flight. A producer now sends one new payment message.
Producer sends the message to the leader
The producer looks up which broker leads the partition and sends a batch there. The message has not been stored anywhere yet.
Leader appends it and its LEO moves to 11
The message gets offset 10. The leader’s LEO is now 11. The HW is still 10, so the message is uncommitted and invisible to consumers.
Followers fetch and append
Each follower asks the leader for data starting at its own LEO, which is 10. The leader sends message 10. Each follower appends it and its LEO becomes 11.
Followers report progress with their next fetch
The follower’s next request asks for offset 11. That request itself tells the leader “my LEO is 11”. No separate progress message is needed.
Leader advances the High Watermark to 11
The leader sees that all three in-sync LEOs are 11. The smallest is 11, so the HW moves to 11. Message 10 is now committed.
Producer receives its acknowledgement
If the producer asked for acks=all, the leader has been holding its request until this moment. It now replies with success.
Consumers can read the message
Any consumer whose position is at or below offset 10 can now fetch it, since it is below the HW of 11.
Followers learn the new High Watermark
The leader’s fetch responses carry its current HW. Followers update their own stored copy on the next round of fetching.
The receipt service, revisited
Back in our payments platform, the receipt service polls the partition. Between steps 2 and 5 it keeps receiving nothing new, because the payment message is above the HW. Only at step 5, when all three brokers hold the message, does the receipt service see it. If the leader had crashed during steps 2 to 4, a follower would have taken over, the producer would have retried, and the receipt service would never have seen a payment that Kafka could lose.
By default, Kafka does not force every message to be flushed to physical disk before continuing. It leaves flushing to the operating system and relies on replication for durability. This is why the HW, which proves that several machines hold the data, is the real safety line rather than “written to disk on one machine”.
FHow Followers Fetch and How the Leader Learns Their Progress
The leader never guesses how far a follower has got. It learns it from the follower’s own requests, and this small trick is the engine behind the High Watermark.
Replication in Kafka is pull-based. The leader does not push data to followers. Instead, each follower runs a background worker, called a replica fetcher, that repeatedly sends fetch requests to the leader. A fetch request says, in effect: “I am replica number 2, and I want data for partition 0 starting at offset 10. Please send me at most this many bytes, and if you have nothing new, wait a little before answering.”
The number inside that request, the starting offset, is the important part. A follower always asks for the next offset it does not have, which is exactly its own Log End Offset. So the fetch offset is a free progress report. When the leader receives a request for offset 11 from a follower, it knows that the follower already holds everything below 11, and therefore that the follower’s LEO is 11.
A student walks up to the teacher and says, “Please give me line 11.” Without saying another word, the student has told the teacher that lines 0 to 10 are already in the notebook. The teacher keeps a small chart on the desk with each student’s next line. Whenever a student comes back, the teacher updates the chart and glances at the smallest number.
sequenceDiagram participant P as Producer participant L as Leader on Broker 1 participant F as Follower on Broker 2 P->>L: Produce one message Note over L: Message gets offset 10, leader LEO goes 10 to 11, HW stays 10 F->>L: Fetch from offset 10 L-->>F: Message 10 plus leader HW = 10 Note over F: Follower LEO goes 10 to 11, follower HW stays 10 F->>L: Fetch from offset 11 Note over L: Leader learns follower LEO = 11, so HW moves to 11 L-->>F: No new data, but leader HW = 11 Note over F: Follower HW moves to 11 L-->>P: Success reply when acks = all
What a fetch request and reply carry
| Direction | Content | Why it exists |
|---|---|---|
| Follower to leader | Partition and fetch offset | Says where to start, and doubles as the follower’s LEO |
| Follower to leader | Maximum bytes and minimum bytes | Controls batch size and avoids tiny, wasteful transfers |
| Follower to leader | Maximum wait time | Lets the leader hold the request until data arrives |
| Follower to leader | Current leader epoch | Lets the leader detect a follower that is out of date about who leads |
| Leader to follower | Message batches | The actual data to copy |
| Leader to follower | Leader’s current High Watermark | How followers learn what is committed |
| Leader to follower | Log start offset | Tells the follower the oldest offset still kept, for retention |
Long polling keeps the system calm
What if the leader has no new data when a fetch arrives? It does not answer with an empty reply and force the follower to ask again instantly, which would flood the network. Instead it parks the request in a waiting area (Kafka calls it a purgatory) and answers as soon as enough new data arrives or the wait time runs out. The default maximum wait for replica fetches is 500 milliseconds. This technique is called long polling, and it gives low delay without wasted traffic.
With pulling, each follower controls its own pace, so a slow follower cannot overwhelm itself, and the leader does not need to track per-follower send buffers. Followers can also batch many messages in one request. This design keeps the leader simple and is a big reason Kafka scales to very high throughput.
Real-world angle: LinkedIn
Kafka was created at LinkedIn to carry activity events such as page views and messages between systems. LinkedIn has publicly described running very large Kafka deployments moving trillions of messages a day. At that size, cheap pull-based replication and long polling are not luxuries. They are what keep the cost of keeping three copies of every event manageable.
MHow the High Watermark Moves Forward
The rule is simple: the HW is the smallest LEO in the ISR. The surprise is in the timing, because followers learn about it one step later than the leader.
Whenever the leader appends data, or receives a fetch from a follower, it may recompute the HW. It looks at its own LEO and at the last known LEO of every follower in the ISR, takes the minimum, and, if that minimum is higher than the current HW, moves the HW up. The HW only moves forward on a healthy leader.
Followers cannot compute the HW themselves, because they do not know each other’s LEOs. They simply copy the value the leader includes in each fetch response. To stay safe, a follower sets its own HW to the smaller of two numbers: the leader’s HW from the reply and its own LEO. It would be wrong for a follower to claim a HW beyond what it has actually written.
A step-by-step example
Start with three in-sync replicas that are fully caught up at LEO 10 and HW 10. Then a producer sends a batch of two messages. Follow the numbers in the table below, row by row.
| Moment | What happens | Leader LEO | F1 LEO | F2 LEO | Leader HW | F1 HW | F2 HW |
|---|---|---|---|---|---|---|---|
| T0 | Everyone is caught up | 10 | 10 | 10 | 10 | 10 | 10 |
| T1 | Producer writes 2 messages to the leader | 12 | 10 | 10 | 10 | 10 | 10 |
| T2 | F1 fetches offsets 10 and 11 | 12 | 12 | 10 | 10 | 10 | 10 |
| T3 | F2 fetches offsets 10 and 11 | 12 | 12 | 12 | 10 | 10 | 10 |
| T4 | F1 asks for offset 12, leader learns F1 = 12 | 12 | 12 | 12 | 10 | 10 | 10 |
| T5 | F2 asks for offset 12, leader learns F2 = 12, HW moves | 12 | 12 | 12 | 12 | 10 | 12 |
| T6 | F1’s next fetch reply carries the new HW | 12 | 12 | 12 | 12 | 12 | 12 |
Notice three things. First, at T2 and T3 the followers already hold the new messages, yet nothing is committed, because the leader does not yet know that. Knowledge travels only with the next fetch. Second, the leader’s HW moves at T5, the moment the slowest in-sync replica reports in. Third, the followers’ own HW values trail the leader’s by one more fetch round.
A follower can hold a message that the leader has already committed while the follower’s own HW still says the message is uncommitted. This gap is tiny, often a few milliseconds, but it was the root cause of a real data-loss bug in older Kafka versions, which we will study in Chapter 15.
Special cases
Only the leader is left
If every follower falls out of the ISR, the minimum is just the leader’s own LEO. Every write is instantly committed, but there is now a single copy. This is the danger zone.
The HW can jump
When a slow follower leaves the ISR, the minimum is computed over fewer replicas, so the HW can leap forward at once and release waiting producers.
Must reach the HW first
A follower re-enters the ISR only after its LEO has reached at least the leader’s HW, so that joining never drags the HW backwards.
Nothing moves
When no new messages arrive, LEO and HW stay equal on every replica. The numbers change only when data flows.
IInside the Broker: How the Numbers Are Really Maintained
So far we watched the numbers from the outside. Now let us step inside one broker and see which parts touch them, and in what order.
A Kafka broker is a busy program with several groups of workers. Network threads accept connections and read incoming requests from the wire. Request handler threads do the real work: appending to logs, reading from logs, and building replies. A component often called the replica manager keeps track of every partition that lives on this broker, and for each one it knows whether the local copy is a leader or a follower. Everything we have discussed happens inside these pieces.
Think of a restaurant kitchen. Order tickets arrive at the pass (network threads), cooks pick them up and prepare dishes (request handlers), and some tickets are clipped to a holding rail because the table’s order cannot leave the kitchen until every dish is ready. That holding rail is what Kafka calls a purgatory. A produce request with acks=all waits on the rail until the High Watermark says all the dishes are done.
The write path on the leader
A handler thread receives the produce request
It checks that this broker is really the leader for the partition and that the request carries an up-to-date leader epoch. Requests sent to the wrong broker are rejected so the client can look up the new leader.
It checks the safety floor
For acks=all, the leader compares the current ISR size with min.insync.replicas. If the ISR is too small, it refuses the write right away, before touching the log.
It appends the batch to the active segment
The batch is written at the end of the newest segment file, assigned the next offsets, stamped with the current leader epoch, and the leader’s LEO moves up. The data lands in the operating system’s memory cache first.
It decides whether to reply now or wait
With acks=1 the reply is sent immediately. With acks=all a delayed operation is created and placed in the produce purgatory, keyed by the partition and the offset it needs the HW to reach.
Later, the HW advances and the waiting request completes
Each time the HW moves, the leader checks the rail and releases every request whose offset is now below the HW. The producer finally gets its success reply.
The read path: who may read how far
Here is a detail that surprises many people. The leader treats followers and consumers differently when serving fetch requests. A follower is allowed to read all the way up to the leader’s LEO, including the uncommitted tail, because copying that tail is exactly its job. A consumer is only allowed to read up to the High Watermark, or up to the Last Stable Offset if it uses read_committed. The same code path serves both, and the difference is simply which limit is applied.
When a follower’s fetch arrives, the handler thread does three extra things beyond reading the data. It records the follower’s fetch offset as that follower’s newest known LEO. It notes the time, so it can later judge whether the follower is still keeping up. And it recomputes the HW, because the follower’s new position might raise the minimum. If the HW did move, the handler completes any produce requests waiting on the rail. Only then does it send the reply, which carries the data plus the leader’s current HW.
Last fetch offset
The leader remembers the offset each follower last asked for. This is its picture of every follower’s LEO, and the input to the HW formula.
Last caught-up time
The leader records the last moment each follower was fully caught up. If that time gets too old, the follower is a candidate for removal from the ISR.
Current ISR and epoch
The leader knows the list of in-sync replicas and the leader epoch it is serving under, and it checks both on every request.
Log segments and indexes
Each segment is a file named after its first offset, with small index files that let the broker jump to any offset quickly without scanning.
Waiting rails, not busy loops
Two kinds of waiting operations exist on a broker. Delayed produce operations wait for the HW to reach a target. Delayed fetch operations wait for enough new data to arrive, which is the long polling from Chapter 7. Both are stored in purgatories that use an efficient timer structure, called a hierarchical timing wheel, so that even hundreds of thousands of waiting requests cost very little to track. Whenever something changes, such as new data appended or the HW moved, the broker checks only the waiting operations tied to that partition, instead of scanning everything.
No thread sits idle waiting for followers. A handler appends, parks the request, and moves on to the next one. Completion is triggered by the event that matters, the HW moving. This event-driven approach is how a single broker serves very large numbers of partitions and clients at once.
Reading the internals from the outside
Each internal mechanism leaves a visible footprint. A growing produce purgatory means the HW is not keeping up with the LEO. A growing delayed-fetch count usually just means consumers are caught up and waiting, which is healthy. Knowing which rail a metric belongs to tells you at once whether you are looking at a problem or at a system working as designed.
PProducer Acknowledgements and the High Watermark
The producer setting called acks decides when the producer is told “your message is safe”. Two of its values are directly tied to our two numbers.
A producer can choose how much confirmation it wants from the cluster before it treats a send as successful. This setting is named acks. Each choice trades speed against safety.
| Setting | Producer is told “success” when | Relation to LEO and HW | Risk |
|---|---|---|---|
| acks=0 | The message has left the producer’s network buffer | None. The producer does not wait for the broker | Loss is possible even with no broker failure |
| acks=1 | The leader has appended the message | Leader LEO has passed the message. HW has not necessarily | Loss if the leader dies before followers copy the message |
| acks=all | The message is committed by every in-sync replica | HW has passed the message | Slower, and needs a healthy ISR |
With acks=1, the teacher says “done” the moment the line is written on the board. With acks=all, the teacher says “done” only after every student in good standing has copied the line. If the teacher then faints, nothing that was announced as done is missing from any notebook.
Where the waiting happens
With acks=all, the leader appends the batch to its log and then parks the producer’s request in a purgatory. It stays there until the High Watermark moves past the last offset of the batch. When that happens, the leader releases the request and sends success. If this does not happen within the producer’s timeout, the producer sees an error and can retry. So the delay of an acks=all write is roughly the time for followers to fetch the data plus the time for the leader to learn about it.
min.insync.replicas: a floor for the ISR
There is a catch. “All in-sync replicas” can shrink to just the leader, and then acks=all would behave like acks=1. To prevent this, Kafka offers min.insync.replicas. It sets the minimum ISR size required for a write with acks=all to be accepted. If the ISR is smaller than this floor, the leader rejects the write with an error rather than pretending it is safe. This protects committed data at the cost of availability for writes.
three copies per partition
at least two copies for a write
wait for the High Watermark
This trio is a widely used starting point for important topics. With it, one broker can fail and writes still succeed, because two in-sync replicas remain. If a second broker fails, the ISR drops below two, and writes are refused, but existing committed data is still readable. An acknowledged message always lives on at least two brokers, so losing any single broker never loses it.
Pros of acks=all
- An acknowledged message is on every in-sync replica
- Survives a leader crash right after the acknowledgement
- Pairs with min.insync.replicas to set a hard safety floor
- The default in current producer clients
Cons of acks=all
- Higher write latency, since the slowest in-sync follower sets the pace
- Writes fail if the ISR falls below the floor
- One sick broker in the ISR can slow many partitions
- Needs careful timeout and retry settings
Since Kafka 3.0, the standard Java producer defaults to acks=all and turns on idempotence, which lets the broker discard duplicate sends caused by retries. Older clients and other languages may still behave differently, so it is worth checking explicitly in your own configuration.
IThe ISR: Who Counts Toward the High Watermark
The High Watermark is only as trustworthy as the ISR behind it. Let us look at how replicas earn and lose their place.
A follower stays in the ISR while it keeps up with the leader. The exact test is time-based. If a follower has not fully caught up with the leader’s log for longer than replica.lag.time.max.ms, the leader declares it out of sync and removes it. In current Kafka versions this setting defaults to 30 seconds; older versions used 10 seconds. Kafka used to also have a rule based on how many messages a follower could lag, but it was removed because bursts of traffic made healthy followers flap in and out.
stateDiagram-v2 [*] --> InSync InSync --> OutOfSync : falls behind for too long OutOfSync --> InSync : catches up to the leader HW InSync --> [*] : broker stops or is removed
Who updates the ISR list?
The leader notices a lagging or recovered follower, but the official ISR list is stored in the cluster’s metadata, which the controller manages. In modern Kafka, the leader sends a request asking the controller to approve the change, and then updates its own state once approved. Older clusters recorded this in ZooKeeper instead. Either way, the effect is the same: the shared record of “who is in sync” is kept consistent, so that after a crash the cluster knows which replicas are safe leaders.
What an ISR change does to the numbers
A follower slows down
Perhaps its disk is busy or its network is congested. Its LEO stops growing while the leader’s keeps climbing.
The HW stalls
Because the HW is the minimum ISR LEO, the slow follower holds it back. Producers using acks=all start waiting.
The timer expires and the follower is removed
After the lag time limit passes, the leader shrinks the ISR. The minimum is now computed without the slow follower.
The HW jumps forward
Stalled messages become committed at once and waiting producers are released, but with one fewer copy behind the commit.
The follower recovers and rejoins
Once its LEO reaches the leader’s HW, the ISR expands and safety returns to normal.
While a follower is out of the ISR, “committed” means fewer copies. If the ISR shrinks to the leader alone and min.insync.replicas is 1, an acks=all write is acknowledged with only one copy. Setting min.insync.replicas to 2 for important topics is what stops this silent weakening.
Common reasons for ISR flapping include long garbage collection pauses on a broker, saturated disks, network problems between racks, overloaded brokers hosting too many partitions, and replication throttles set too low during maintenance. Frequent shrinks and expands are therefore one of the best early warning signs in a cluster, and we will return to them in the monitoring chapter.
LThe Lifecycle of a Replica
A replica is not just “in the ISR” or “out”. It is born, catches up, works, sometimes leads, and eventually retires. Following that path shows how LEO and HW behave in each stage.
Consider what happens when a cluster grows. An operator adds a new broker and asks Kafka to move some partitions onto it. For each moved partition, a brand-new replica is created on the new machine. It starts with an empty log, so its LEO is at the beginning, while the leader’s LEO may be in the millions. The stages that follow are the same ones a recovering replica goes through after a long outage.
Assigned
The controller records that the partition should also live on the new broker. The new replica exists but has no data, and it is not in the ISR, so it has no say in the High Watermark.
Catching up
The new replica sends fetch requests starting from the earliest offset the leader still keeps. Its LEO climbs quickly toward the leader’s. For a large partition this can take minutes or hours, and it is usually throttled to protect normal traffic.
Caught up
When the replica’s LEO reaches at least the leader’s High Watermark and it keeps pace, the leader asks to add it to the ISR. From this moment its LEO counts in the minimum.
In sync and serving
The replica is now a full member. It follows the leader, learns the HW from each reply, and can be chosen as leader if the current one fails.
Leader (optional)
If elected, the replica gets a higher leader epoch, serves producers and consumers, and starts computing the HW from its followers’ fetches.
Retired
If the partition is later moved away, the controller removes the replica from the assignment once others are in sync, and the broker deletes its local files.
A new student joins the class halfway through the year. They start with an empty notebook and copy the earlier lessons as fast as they can. Until they have caught up, the class does not wait for them, and the official record does not depend on them. Once they are up to date, they join the group of students in good standing, and from then on the slowest of that group sets the pace.
Why newcomers stay out of the ISR
Keeping a catching-up replica out of the ISR is a deliberate safety rule. If a replica with LEO 0 were counted immediately, the minimum would drop to 0 and the High Watermark would collapse, hiding the whole log from consumers and stalling every acks=all producer. Waiting until the replica reaches the HW means that joining the ISR can never pull the watermark backwards.
| Stage | In the ISR? | Counts toward the HW? | Can become leader? |
|---|---|---|---|
| Assigned, empty | No | No | No |
| Catching up | No | No | Only by unclean election |
| In sync | Yes | Yes | Yes |
| Fell behind | Removed | No | Only by unclean election |
| Leader | Yes | Its own LEO is part of the minimum | Already is |
During a reassignment, a partition may briefly have more replicas than its replication factor, because the old replica stays until the new one is fully in sync. This is safe and intentional, but it means extra network and disk use at that time, which is one more reason to throttle and to move partitions in small groups.
WWhat Consumers Can See
A consumer’s view of a partition ends at the High Watermark. Understanding this explains lag numbers, delays and a few surprising tool outputs.
A consumer keeps a position: the offset of the next message it wants. It sends fetch requests to the partition leader with that offset. The leader returns messages starting there, but only those below the HW. If the consumer is fully caught up and asks for an offset equal to the HW, the leader holds the request briefly and answers when new committed data appears.
Periodically, the consumer saves its progress by committing an offset. Kafka stores these commits in an internal topic, so a restarted consumer resumes where it stopped. The committed offset is the consumer’s bookmark. It is a different thing from the HW, even though both are offsets in the same partition.
Consumer lag
Consumer lag is the distance between what is available to read and what the consumer has finished. In simple terms it is the latest readable offset minus the consumer’s committed offset. A lag of 5,000 means there are 5,000 messages waiting. Lag is the single most watched number in most streaming systems, since growing lag means consumers cannot keep up.
The standard command-line tool that lists consumer groups shows a column named LOG-END-OFFSET. For normal consumers, the value it reports is in practice the latest offset they are allowed to read, which is the High Watermark, not the raw LEO of the leader. The two are usually equal or very close in a healthy cluster, but during replication delays they can differ.
Transactions and the Last Stable Offset
Kafka also supports transactions, where a producer writes several messages that should become visible together or not at all. Consumers configured with the read_committed isolation level must not read messages from a transaction that is still open. For them, Kafka uses another boundary called the Last Stable Offset (LSO): the offset before the first still-open transaction. The LSO is always less than or equal to the HW. So a read_committed consumer sees a slightly shorter log than a default one, and both see a shorter log than the leader’s LEO.
| Boundary | Meaning | Who is limited by it |
|---|---|---|
| Log start offset | Oldest offset still kept after retention | Anyone reading from the beginning |
| Last Stable Offset | Before the first open transaction | Consumers using read_committed |
| High Watermark | Before the first offset not on all in-sync replicas | Default consumers, and acks=all producers |
| Log End Offset | Next offset to be written on a replica | Replication and monitoring, not consumers |
Reading from the nearest replica
By default all reads go to the leader, which can be in a different data centre zone from the consumer and cause cross-zone network cost. Since Kafka 2.4, a feature often called follower fetching lets a rack-aware consumer read from a nearby follower instead. The follower serves only offsets below its own HW. Since a follower’s HW trails the leader’s by a fetch round, data reaches a follower-reading consumer a little later, a small price for lower network cost and shorter distance.
Real-world angle: multi-zone deployments
Companies running Kafka across several availability zones in a cloud region often pay for data crossing zone boundaries. Follower fetching lets consumers in a zone read from a replica in the same zone, reducing that traffic. The trade-off is the slightly delayed HW on the follower, which is usually acceptable for analytics and search indexing, but should be checked for very latency-sensitive paths.
EWhen the Leader Fails: Election and the High Watermark
This is the moment the High Watermark earns its keep. It draws the exact line between data that must survive and data that may be lost.
When the broker holding a partition’s leader crashes or becomes unreachable, the cluster’s controller notices and picks a new leader from the ISR. Only in-sync replicas are eligible in the normal case, because they are guaranteed to hold every committed message. The controller records the decision, increases a counter called the leader epoch (we will explain it soon), and informs the brokers involved.
flowchart TD
A["Leader on Broker 1 fails"] --> B["Controller checks the ISR list"]
B --> C{"Is another in-sync replica alive?"}
C -->|"Yes"| D["Pick it as new leader and raise the leader epoch"]
C -->|"No"| E{"Is unclean election allowed?"}
E -->|"No"| F["Partition stays offline, no committed data lost"]
E -->|"Yes"| G["Out-of-sync replica leads, committed data may be lost"]
D --> H["Committed messages are safe on the new leader"]
The teacher suddenly leaves the room. The class picks a new teacher from among the students in good standing, since they all have every official line. A student who was absent for half the lesson is not chosen, because they would teach an incomplete version as if it were the whole.
What happens to each zone of the old log
Committed and safe
Every in-sync replica had these messages, so the new leader has them too. Consumers can carry on reading without any gap.
Possibly lost
Some of these messages may exist only on the dead leader. They were never acknowledged to acks=all producers, so those producers will retry them.
The new leader’s log end becomes the new truth. Any follower that has messages beyond it must cut them off. The new leader also starts with whatever HW it last learned, which may be a little lower than the real one. It recomputes the HW as followers reconnect and fetch. This can cause a short pause in which consumers see no progress, a normal and brief effect of a leader change.
| Failure | What Kafka does | Committed data lost? |
|---|---|---|
| Leader crashes, other ISR members alive | A new leader is chosen from the ISR | No |
| A follower crashes | It is dropped from the ISR after the lag limit; the HW moves on without it | No, but fewer copies exist |
| All ISR members crash, unclean election off | Partition stays offline until an ISR member returns | No, but the partition is unavailable |
| All ISR members crash, unclean election on | A lagging replica becomes leader | Yes, possibly |
Unclean leader election: choosing availability over safety
The setting unclean.leader.election.enable answers this question: if no in-sync replica is alive, should the cluster wait, or promote an out-of-sync replica? The default is off, meaning Kafka waits and protects the data. Turning it on makes the partition available again sooner, but the promoted replica may be missing messages that consumers already read, and those messages can vanish or be overwritten by new ones. This is a conscious trade between the letters C and A of the CAP theorem: consistency of committed data versus availability of the partition.
For payments, orders and any system where a lost event is costly, leave unclean election off. For a metrics or log topic where a gap is acceptable and uptime matters more, enabling it can be a reasonable and conscious choice.
TTruncation, the Old Bug, and Leader Epochs
For years, Kafka used the High Watermark to decide how much of a follower’s log to trust after a restart. That shortcut had a flaw, and the fix is one of the most elegant ideas in Kafka replication.
When a follower restarts or becomes a follower again, it must make sure its log is a true prefix of the new leader’s log. Any extra messages it holds that the leader lacks must be removed. That removal is truncation. In early versions, the follower truncated its log to its own stored High Watermark, on the theory that everything below the HW was certainly committed.
The theory ignored the one-round delay from Chapter 8. A follower’s stored HW can lag behind the truth. So a follower could truncate messages that were in fact committed and acknowledged.
The data-loss scenario in plain steps
Two replicas, one new message
Broker A is leader, broker B is follower. Both have message m1 at offset 0. A producer writes m2 to A at offset 1.
B copies m2 but has not heard the good news
B fetches m2, so its LEO is 2. On its next fetch, A learns B’s LEO and moves its HW to 2, then acknowledges the producer. B’s own stored HW is still 1.
B restarts and truncates to its stale HW
B goes down and comes back. Following the old rule, it cuts its log back to offset 1, throwing away m2.
A fails before B can fetch m2 again
With A gone, B, still in the ISR, becomes leader holding only m1. The acknowledged message m2 no longer exists anywhere.
A second scenario was possible too, called replica divergence. After certain crash sequences, two replicas could end up with different messages at the same offset, and each believed it was in sync. That is arguably worse than losing data, because the copies silently disagree.
The fix: leader epochs
Kafka 0.11 introduced the leader epoch. It is a number attached to each partition that increases by one every time a new leader is elected. Every batch of messages is stamped with the epoch of the leader that wrote it, and every replica keeps a small table recording, for each epoch, the offset at which that epoch began.
Imagine each new teacher starts a fresh chapter in the class notebook and writes the chapter number at the top. If a returning student is unsure which of their lines are valid, they do not guess. They ask the current teacher, “Where did chapter 3 end?” and cut their notebook exactly there.
sequenceDiagram participant F as Restarted follower participant L as Current leader F->>L: Where did my latest epoch end? L-->>F: That epoch ended at offset 5 Note over F: Truncate anything after offset 5 F->>L: Fetch from offset 5 L-->>F: Send messages from offset 5 onward
On restart, the follower asks the leader for the end offset of the latest epoch the follower knows about. The leader answers from its own history. The follower truncates to that answer, or to its own LEO if that is smaller, and then resumes fetching. Since the answer comes from the leader’s real log rather than from a possibly stale HW, acknowledged messages are no longer thrown away, and diverging logs are detected and repaired.
Later versions extended the idea to clients. Since Kafka 2.1, consumers remember the epoch of the data they have read, so after a leader change they can detect that the log has been truncated beneath them and react, rather than silently continuing from a wrong position.
Why this matters in practice
Large clusters restart brokers all the time: rolling upgrades, security patches, cloud instance replacements. With thousands of partitions per broker, even a rare timing window happens somewhere sooner or later. Leader epochs turned a sleeping bug into a solved problem, which is why every serious operator runs a Kafka version well past 0.11.
CCheckpoint Files and Recovery After a Restart
A broker’s memory disappears when it restarts, but the High Watermark and its friends must not. Kafka saves them in small checkpoint files.
Each broker keeps a few tiny files that act as bookmarks. They are rewritten regularly so that after a restart the broker does not have to start from zero. Think of them as sticky notes stuck to each partition’s folder. They are hints for recovery, not the source of truth, since the truth is always the log itself and the leader’s answers.
replication-offset-checkpoint
Stores the last known High Watermark of each partition on the broker. It is refreshed every few seconds by default, so it can be slightly behind the live value.
recovery-point-offset-checkpoint
Records up to which offset data is known to have been flushed to disk. After an unclean shutdown, only the part beyond this point needs to be checked for damage.
log-start-offset-checkpoint
Remembers the oldest offset still kept in each partition, so that old data removed by retention is not mistaken for missing data.
leader-epoch-checkpoint
Lives inside each partition folder and stores the table of leader epochs and their starting offsets, used for truncation as explained in Chapter 15.
What happens when a broker starts
Detect how the last shutdown ended
After a clean shutdown, the broker leaves a marker and can skip heavy checks. After a crash there is no marker, so it treats the recent tail of every log as suspect.
Recover the log tail
The broker scans the segments beyond the recovery point, drops any half-written or corrupted messages at the end, and rebuilds index files if needed.
Set the LEO from the log itself
The LEO of each replica is simply the end of the recovered log, plus one. No file needs to store it. The log is the authority.
Load the saved HW, never above the LEO
The HW is read from its checkpoint but is capped at the LEO, since it cannot be ahead of what is written.
Rejoin as follower or leader
If another broker now leads, this replica truncates using leader epochs and starts fetching. If it must lead, it recomputes the HW as followers reconnect.
A student returns after a break. The sticky notes on the notebook say roughly where they stopped, but they check the last few pages for smudges, count the real number of filled lines, and then ask the teacher which chapter ended where before continuing. The notes save time, but the notebook and the teacher decide the facts.
Planned restarts are gentler
Operators restart brokers constantly for upgrades. Kafka supports controlled shutdown: before a broker stops, it asks the controller to move leadership of its partitions to other in-sync replicas. Producers and consumers see a brief blip instead of an outage, and no ISR has to shrink because of a surprise. This is why rolling upgrades restart one broker at a time and wait for the cluster to be fully in sync before moving to the next.
Restarting several brokers together can push partitions below min.insync.replicas or leave no in-sync replica for some partitions. Waiting until the under-replicated partition count is zero between restarts keeps the High Watermark and the ISR healthy.
FFour Failure Stories With Real Numbers
Definitions are useful, but understanding sticks when you follow numbers through a crisis. Here are four short stories, each using the safe trio: replication factor 3, min.insync.replicas 2, acks=all.
Story 1: the sleepy follower
A partition has its leader on Broker 1 and followers on Brokers 2 and 3. All three LEOs are near 5,000. Then Broker 3 hits a long garbage collection pause and stops fetching. Its LEO freezes at 5,000 while the leader keeps accepting writes, reaching 5,300. The HW is the minimum, so it also freezes at 5,000. Producers using acks=all start seeing their sends wait, because nothing new is committed.
After the lag time limit, the leader removes Broker 3 from the ISR. The ISR now has two members, Broker 1 (LEO 5,300) and Broker 2 (LEO 5,300). The minimum is 5,300, so the HW jumps to 5,300 and every waiting producer is released at once. The ISR size of 2 still meets the floor, so writes continue. A few minutes later, Broker 3 wakes up, catches up, and rejoins the ISR. Users saw a short latency spike and nothing else.
The stall lasted as long as the lag time limit. Lowering it removes sleepy followers sooner but risks removing healthy ones during brief pauses. There is no perfect value, only a trade you should choose deliberately.
Story 2: the leader dies with an uncommitted tail
Now the leader on Broker 1 has LEO 1,000. Broker 2 has LEO 995. Broker 3 has LEO 990. The HW is 990. The leader crashes. The controller picks Broker 2, which is in the ISR, as the new leader, and raises the leader epoch. Broker 2’s log ends at 995, so that is the new end of history.
Look at the messages at offsets 990 to 994. They were uncommitted under the old HW, so their producers never received success. But Broker 2 holds them, so they survive the failover, and they will become committed once Broker 3 copies them. The messages at offsets 995 to 999 exist only on the dead Broker 1. When it eventually returns, it asks the new leader where its epoch ended, learns that the answer is 995, and truncates offsets 995 to 999 away.
Messages between the HW and the LEO may survive or disappear depending on which replica wins the election. The producers of both groups received errors or timeouts and will retry. Without idempotent producers, the retried messages from 990 to 994 could appear twice. This is exactly why idempotence and safe consumers are part of the standard advice.
Story 3: the isolated old leader
A network fault cuts Broker 1, the leader, off from the rest of the cluster while it keeps running. From its own point of view nothing has changed, and it still believes it leads. Meanwhile the controller cannot reach it, elects Broker 2 as the new leader, and raises the leader epoch. Some producers, whose information is stale, still send data to Broker 1.
With min.insync.replicas set to 2, Broker 1 cannot safely commit anything. Its followers no longer fetch from it, so its HW cannot move, and acks=all requests just wait and then fail. This is the safety floor doing its job. When the network heals, Broker 1 learns about the higher epoch. Requests that still carry the old epoch are rejected as coming from a stale leader, and Broker 1 becomes a follower, truncates its uncommitted tail using the epoch answer, and resumes copying from the true leader. Two leaders never both commit data.
Story 4: the broker with a dead disk
Broker 3 loses its disk entirely and is replaced with a fresh machine with the same identity. The ISR for its partitions shrinks to two brokers. The partitions keep working with two copies, and the safe trio still allows writes. The new machine starts fetching every partition from the earliest available offset, and each of its replicas climbs from LEO 0 toward the leader’s LEO.
During this catch-up, the cluster is running at reduced protection. If another broker in one of those ISRs failed now, only one in-sync replica would remain, the ISR would fall below the floor, and writes to that partition would be refused. Committed data would still be safe on the survivor. When the new replicas reach the HW, they rejoin the ISR and full protection returns.
| Story | What moved the HW | Was committed data at risk? | Main lesson |
|---|---|---|---|
| Sleepy follower | Removal from the ISR made it jump forward | No | Lag time limit sets the size of the stall |
| Leader dies | New leader recomputes from its followers | No | Uncommitted messages may survive and duplicates need idempotence |
| Isolated old leader | Could not move at all | No | The safety floor and epochs prevent two leaders |
| Dead disk | Unchanged; new replica excluded | No, but margin is thinner | Recovery windows run with fewer copies |
TConsistency, Durability and the Trade-offs Behind the Design
Kafka’s replication is a set of deliberate choices. Knowing what it gives, and what it costs, helps you decide when it is the right tool.
Many well-known databases use a majority quorum. With five replicas they consider a write committed when three of them have it, and they tolerate two failures. Kafka takes a different route with the ISR. A write is committed when all in-sync replicas have it, and the ISR itself can shrink. The two approaches make different trades.
| Question | Majority quorum (for example Raft) | Kafka’s ISR approach |
|---|---|---|
| Copies needed to survive 2 failures | 5 | 3 |
| Commit rule | A majority has the write | Every in-sync replica has the write |
| Effect of one slow replica | Hidden, because the majority is enough | Adds delay until it is removed from the ISR |
| Storage cost per byte kept safely | Higher | Lower |
| Extra machinery | Built into the protocol | Needs the controller to manage the ISR list |
The ISR model needs fewer copies for the same failure tolerance, which matters when each partition may hold terabytes. The cost is that one struggling follower can slow writes until the lag limit removes it. Kafka accepts that cost because copies are expensive and because the removal rule bounds the damage.
A majority quorum is like a committee that approves a decision when most members vote yes, so a member who is late does not delay anything. The ISR approach is like a study group that waits for everyone currently attending before moving on, but politely sends home anyone who has been absent too long. The group needs fewer members to be safe, but a late member can briefly hold everyone up.
What Kafka guarantees, and what it does not
Guarantees
- Order of messages inside one partition
- A committed message is not lost while at least one in-sync replica survives
- Consumers see the same history in the same order
- Configurable durability, per topic, through three settings
Does not guarantee
- Order across different partitions of a topic
- Safety for messages above the High Watermark during a failover
- Zero duplicates without idempotent producers or transactions
- Availability and consistency at once, under an unclean election
The CAP view in one paragraph
The CAP theorem says that when a network splits a system, you must choose between staying consistent and staying available. Kafka’s defaults lean toward consistency for committed data: it refuses writes when the ISR is below the floor, and it keeps a partition offline rather than electing an out-of-date leader. Each of these choices can be relaxed per topic. Writes with acks=1 favour availability and speed, and unclean election favours availability at the cost of possible loss. The High Watermark is the mechanism that keeps the consistent side of that choice honest.
There is no universally right setting. A payments topic and a click-tracking topic should not share the same durability settings. Write the reasoning down next to the topic’s configuration so that the next engineer understands the trade you made.
PPerformance and Scalability of Replication
Replication is not free. Every byte written to a leader must be copied to its followers, and that copying decides how quickly the High Watermark can move.
Suppose a topic receives 100 megabytes per second of new data and has a replication factor of 3. The leaders receive 100 megabytes per second from producers, and must send out another 200 megabytes per second to two followers each. On top of this come the consumers’ own reads. In short, replication multiplies network use by roughly the replication factor minus one, so capacity planning must include it.
Why acks=all latency is a chain
The time for a message to be committed is roughly: leader append time, plus the wait for each follower’s fetch to arrive, plus the follower’s append time, plus one more fetch to report back. The slowest in-sync follower sets the pace, because the HW is the minimum. This is why a single overloaded broker can raise tail latency for all the partitions it follows, even if their leaders are healthy.
Replica fetcher threads
The setting num.replica.fetchers controls how many fetcher threads a broker uses per source broker. More threads can keep up with many partitions, at the cost of more connections and CPU.
Fetch sizes and waits
Settings such as replica.fetch.max.bytes, replica.fetch.min.bytes and replica.fetch.wait.max.ms shape how large and how frequent follower fetches are. Larger, less frequent fetches raise throughput but can add a few milliseconds of delay.
Page cache and zero-copy
Kafka serves recent data straight from the operating system’s memory cache and sends it to the network without copying through the application. Caught-up followers therefore read almost entirely from memory.
Rack-aware replicas
Spreading replicas across racks or zones protects against group failures, but adds network distance to the fetch loop, which is worth measuring.
Replication throttles
When moving partitions between brokers, Kafka can limit replication bandwidth so that catch-up traffic does not starve normal followers and cause ISR shrinks.
Compression and batching
Producers that batch and compress reduce bytes on the wire and on disk, which shrinks both the replication load and the time to commit.
Scaling out
Kafka scales by adding partitions and brokers. More partitions spread leaders across more machines, which lifts total throughput. But every partition means more replication state, more fetch work and more files. Very high partition counts per broker slow down leader elections and recovery, because every partition needs its own epoch handling and checkpoint. Good designs keep partition counts sensible and grow the cluster horizontally before a single broker becomes crowded.
Watch the gap between leader LEO and follower LEO under peak load. If it stays close to zero, replication is healthy and the HW tracks the leader closely. If the gap grows during bursts and shrinks slowly afterwards, add network capacity, brokers or fetcher threads before the ISR begins to flap.
SSecurity Around Replication
The High Watermark protects the integrity of your history. Protecting who can read or change it is a separate job.
Replication traffic is just brokers talking to each other, so it needs the same care as any internal service traffic. Brokers should authenticate to one another, and the link between them should be encrypted. Kafka lets you configure a dedicated listener for communication between brokers, separate from the one clients use, so you can apply stricter rules to it and keep it away from the public network.
Encrypt the replica link
Use TLS on the inter-broker listener so that copies of your data moving between brokers cannot be read or altered on the network.
Authenticate every broker
Each broker should prove who it is, for example with certificates or SASL, so a stranger cannot pose as a follower and pull your data.
Limit what principals can do
Brokers need a cluster-level permission to replicate. Applications should get only the read or write access they need on specific topics.
Protect the disks
Every replica stores a full copy, so disk encryption and file permissions on every broker matter as much as on the leader.
Note the link between safety settings and security. A fake follower that fetches data is really just a reader with access to everything. An unclean leader election can also be seen as an integrity risk, since it may let unreplicated, out-of-date history become the official one. Keeping unclean election off on sensitive topics is therefore a data protection choice as well as an availability choice.
Broker-to-broker ports should be reachable only by other brokers and trusted management tools. Treat them as internal infrastructure, exactly like a database’s replication port.
MMonitoring: Reading the Numbers
Because LEO and HW describe the health of replication, watching them, and the metrics built on them, reveals trouble long before users notice.
Kafka exposes many metrics through JMX and monitoring agents. You do not need all of them. A small set tells the story of replication health, and most on-call playbooks start there.
| Metric | What it tells you | What to do |
|---|---|---|
| UnderReplicatedPartitions | Partitions where the ISR is smaller than the replica list | Alert if above zero for more than a short time. Look for a slow or down broker |
| UnderMinIsrPartitionCount | Partitions with fewer in-sync replicas than min.insync.replicas | Treat as urgent. Writes with acks=all are failing for these |
| OfflinePartitionsCount | Partitions with no leader at all | Page immediately. Reads and writes are blocked |
| IsrShrinksPerSec and IsrExpandsPerSec | How often followers fall out of and rejoin the ISR | Frequent pairs signal flapping. Shrinks without expands signal a lasting problem |
| Replica fetcher MaxLag | The largest gap between leader and follower on this broker | A growing value means a follower cannot keep up |
| Per-partition LogEndOffset | The LEO of each replica, from every broker | Compare across replicas to see exactly which follower lags |
| UncleanLeaderElectionsPerSec | Elections that chose an out-of-sync replica | Any non-zero value on a critical topic deserves an investigation |
| Produce purgatory size | Producer requests waiting for the HW to advance | A rising queue points to slow followers or an ISR problem |
| Consumer lag | How far consumers are behind the readable end of the log | Alert on growth, not just on absolute size |
A simple diagnosis path
Producers report timeouts
Start by checking whether the affected topics use acks=all and whether their partitions appear in the under-min-ISR count.
Compare replica LEOs
For an affected partition, list the LEO on each replica. The replica with a much smaller value is the slow one, and it is holding the HW back.
Inspect that broker
Check disk busy time, network errors, garbage collection pauses and CPU on that one machine. The cause is usually one of these.
Confirm recovery
Look for the ISR expand event, under-replicated count returning to zero, and the purgatory queue draining. Then review why it happened so it does not repeat.
A single ISR shrink during a deployment is normal. A steady stream of shrinks on the same broker at the same hour each day, for example during a backup job, is a pattern worth fixing before it causes an incident.
RReal-World Usage
Every company that runs Kafka at scale lives with these two numbers, even if their dashboards only show the friendly summaries built on top.
LinkedIn: activity streams and the log as a backbone
LinkedIn built Kafka to move activity events such as page views, searches and messages between systems, and has publicly described handling trillions of messages per day. At this scale, replication settings are a daily conversation. Critical pipelines use strong acknowledgements and multiple replicas across racks, while lower-value telemetry can choose faster, lighter settings. The High Watermark is what lets downstream teams trust that the events they read will still be there tomorrow.
Uber: trips, dispatch and pricing signals
Uber has publicly written about using Kafka as the messaging backbone for trip events and real-time data across many services, and about building tooling for replication across clusters and regions. A rider’s trip state changing from “requested” to “matched” is exactly the kind of fact that should not be read before it is safely replicated. Consumers reading only committed data avoids showing one service a state that another service could lose in a failover.
Netflix: a data pipeline for events
Netflix has described a large event pipeline, often called Keystone, that collects and routes viewing and operational events, with Kafka at its centre. Different streams carry different value: playback errors and business events are treated more carefully than high-volume debug data. Netflix’s public talks stress that different topics deserve different durability settings, which maps directly onto choosing acks, replication factor and min.insync.replicas per topic.
An illustration: a bank’s payment events
This example is a simplified illustration, not a description of any specific bank. Suppose a payments team writes every debit and credit to a topic with a replication factor of 3, min.insync.replicas of 2, acks=all and unclean election off. When a broker’s disk fails at midday, the leader for some partitions moves to another in-sync replica, and producers retry unacknowledged sends. Reconciliation shows no missing payments, because every acknowledged event was below the High Watermark, meaning on at least two brokers, before the producer was told it succeeded.
The lesson from these companies is not that everyone uses the same settings. It is that settings are chosen per topic based on how costly a lost or delayed event is, using the same two underlying mechanisms.
DDesign Patterns and Anti-patterns
Good replication design is mostly a handful of proven combinations, plus a list of tempting shortcuts to avoid.
Patterns that work
The safe trio
Replication factor 3, min.insync.replicas 2, acks=all. It survives one broker failure with writes still accepted and never acknowledges a message held by fewer than two brokers.
Idempotent producer with retries
Let producers retry on errors, with idempotence on, so that retries after a leader change do not create duplicates.
Spread across failure domains
Place replicas on different racks or zones, so one power or network failure cannot remove the whole ISR at once.
Per-topic policies
Set durability strictly for important topics and relax it for telemetry, instead of one setting for everything.
Controlled, sequential restarts
Restart one broker, wait for full sync, then continue. Leaders move away gracefully and the ISR stays healthy.
Idempotent consumers
Design consumers to handle a message twice safely, since failures can lead to reprocessing even when Kafka itself is correct.
Anti-patterns to avoid
What people do
They use acks=1 for important data because it looks fast in a benchmark.
Why it hurts
The producer hears “success” as soon as the leader has written the message, which happens before followers copy it. If the leader dies in that gap, the message is gone even though the producer believed it was safe.
Do this instead
Use acks=all for anything you cannot recreate, and measure the real latency impact. It is usually smaller than expected.
What people do
They set acks=all but leave min.insync.replicas at 1.
Why it hurts
If followers fall out of the ISR, the ISR can shrink to the leader only. The HW then equals the leader’s LEO, so acks=all behaves exactly like acks=1. The team believes it has strong safety but does not.
Do this instead
Set min.insync.replicas to 2 with a replication factor of 3, and alert on partitions that fall below it.
What people do
They enable unclean leader election cluster-wide to make outages shorter.
Why it hurts
Under bad luck, an out-of-date replica becomes leader. Committed messages disappear, and new messages reuse their offsets, so consumers may skip or see different data at the same positions.
Do this instead
Keep it off by default. Enable it only on specific topics where availability clearly beats consistency.
What people do
They restart or replace several brokers at once during maintenance.
Why it hurts
Some partitions lose more than one replica together. The ISR falls below the minimum, writes are refused, and in the worst case no in-sync replica remains, which takes the partition offline.
Do this instead
Go one broker at a time, and wait for the under-replicated count to reach zero before moving on.
BBest Practices and Common Mistakes
A short checklist for design reviews and a table of mistakes that show up again and again.
Classify each topic by how costly a loss is
Money, orders and audit trails need the strictest settings. Clickstreams and debug logs can be more relaxed. Decide once, write it down, and apply it per topic.
Set replication factor, min ISR and acks together
These three settings work as a set. Reviewing one in isolation leads to false confidence.
Spread replicas across failure domains
Use rack or zone awareness so that one incident cannot remove all in-sync replicas.
Monitor ISR health, not just broker uptime
A broker that is up but too slow to stay in sync is as harmful as one that is down, and only the ISR metrics show it.
Practise failure
Regularly stop a broker in a test environment and watch the ISR, the HW and consumer lag. Teams that have seen it once handle real incidents calmly.
Keep Kafka versions current
Many replication fixes, including leader epochs, only exist in newer releases. Old clusters carry old bugs.
| Common mistake | What goes wrong | Better approach |
|---|---|---|
| Assuming written means safe | Data above the HW can vanish in a failover | Rely on acks=all and the HW for safety |
| Forgetting the plus one in LEO | Off-by-one errors in tooling and reasoning | Remember LEO is the next offset, not the last |
| Replication factor of 1 in production | Any disk or broker failure loses data | Use at least 3 for important topics |
| Ignoring slow followers | Higher latency and repeated ISR flapping | Investigate the broker, not just the topic |
| Comparing consumer lag to the raw LEO | Small mismatches during replication delays | Measure lag against the readable end |
| Treating retries as harmless | Duplicates when idempotence is off | Enable idempotent producers and make consumers idempotent |
MMyths, Traps and Gotchas
Some beliefs about these two numbers feel obvious and are quietly wrong. Here are the ones that cause the most confusion in design meetings and incident calls.
“Committed means written to disk”
Committed means present on every in-sync replica, usually in their memory caches. The operating system flushes to disk soon after. Safety comes from having several machines hold the data, not from a flush on one.
“The HW is the same on every replica”
Only the leader’s value is authoritative. Followers hold a copy that trails by a fetch round, so two replicas can report different HW values at the same instant, and both are behaving correctly.
“acks=all means all replicas”
It means all in-sync replicas. If the ISR has shrunk to one, that is one copy. Only min.insync.replicas turns “all in sync” into a real minimum.
“Uncommitted data is always lost in a failover”
It may survive, if the new leader holds it. It is undecided, not doomed. That uncertainty is exactly why producers must be ready to retry safely.
“Consumer lag is measured against the raw LEO”
Lag is measured against what the consumer is allowed to read, which is the HW, or the LSO for read_committed. During a replication delay the raw LEO is further ahead than what consumers can see.
“A higher replication factor always means safer”
More copies help only if the settings use them. With min.insync.replicas at 1 and acks=1, five replicas can still lose data on a single leader crash. Copies must be paired with the acknowledgement rules.
“The leader’s LEO never goes down”
A healthy leader appends only. But a broker that used to lead and is now a follower can see its LEO move backwards after truncation, which is normal and expected.
“Restarting a broker is always harmless”
A gentle restart is harmless. Restarting several at once, or restarting while another replica is still catching up, can push partitions below the safety floor.
Gotchas worth writing on the wall
Tools disagree on whether an offset they print is inclusive or exclusive. Before comparing a LEO, an HW and a consumer position, confirm which convention each value uses. A difference of one is often a definition issue and not a fault.
Cluster-wide averages of replica lag look fine when one broker is struggling. Always look at the worst replica per partition, since the slowest in-sync replica is the one that controls the High Watermark.
Raising min.insync.replicas or switching a producer to acks=all protects future writes. It cannot make earlier writes safer. If data mattered yesterday, the settings needed to be right yesterday.
DPutting It Together: Designing Settings for Real Topics
Let us apply everything to a practical exercise. A company runs one cluster with four kinds of topics. Each needs different answers to the same questions.
Start by asking four questions about any topic. How costly is it to lose one message? How much delay can the writers tolerate? How many broker failures must the topic survive? And do consumers act on the data in ways that cannot be undone? The answers give you the three settings from Chapter 10 and the election policy from Chapter 14.
Price the loss
Is a missing event a small statistical wobble, or money, a legal record, or a customer promise? This one answer drives most of the rest.
Set the delay budget
Ask how long a producer may wait for an acknowledgement. If a few extra milliseconds are fine, acks=all is almost always affordable.
Count the failures to survive
Decide whether one broker, or a whole rack or zone, may fail. That gives the replication factor and the spread of replicas.
Decide the outage behaviour
If every in-sync replica is gone, is it better to wait for one to return, or to accept a possibly incomplete leader? This sets unclean election.
| Topic (illustration) | Cost of loss | Replication factor | min.insync.replicas | acks | Unclean election |
|---|---|---|---|---|---|
| Payment events | Very high | 3 | 2 | all | Off |
| Audit trail | Very high, legal | 3 | 2 | all | Off |
| Website clickstream | Low per event | 3 | 1 or 2 | 1 or all | Off |
| Device debug metrics | Very low | 2 | 1 | 1 | May be on |
The reasoning behind each row
Payment events use the safe trio. Every acknowledged message is on at least two brokers, one broker can fail without stopping writes, and a partition without an in-sync replica waits rather than gambling. Producers are idempotent, and consumers that trigger irreversible actions use read_committed if the pipeline is transactional.
The audit trail is treated the same way, with attention to retention time and to replicas spread across zones, since these records must be there when someone asks for them years later.
Website clickstream can tolerate losing a handful of events, but not losing whole partitions during an outage, so it keeps three replicas and leaves unclean election off. Whether to use acks=all depends on the delay budget. Many teams choose acks=1 here to keep the front-end snappy and accept a tiny loss window.
Device debug metrics are high in volume, low in value, and useful mainly in aggregate. Two replicas and light acknowledgements keep costs down. Allowing unclean election is a conscious choice that keeps the topic writable through outages, at the risk of a gap in a chart.
A sanity check for any row
Ask this out loud in the design review: “Suppose one broker dies right now, right after a producer receives success. Can I still find the message?” If the answer is “yes, always” for the payment row and “probably not, and we accept that” for the metrics row, then your settings match your intent.
The table is an illustration to show the method. Real teams adjust it to their own hardware, failure domains, regulations and budgets. The method stays the same: price the loss, set the delay budget, count the failures, decide the outage behaviour.
FFrequently Asked Questions
Short answers to the questions people ask most often when these two numbers first click.
SSummary and Key Takeaways
Two numbers, one simple idea: write first, commit when everyone who matters has a copy.
Every replica of a Kafka partition keeps a Log End Offset that says how much it has written. The leader combines the LEOs of all in-sync replicas into a High Watermark that says how much is safe. Producers, consumers, leader elections and restarts all depend on the gap between those two lines. Understanding that gap turns many mysterious Kafka behaviours, such as stalled acknowledgements, sudden lag, or surprising data after a failover, into predictable outcomes.
Key Takeaways
- LEO is the next offset a replica will write. Each replica has its own, and it equals the last offset plus one.
- HW is the smallest LEO among in-sync replicas. Messages below it are committed, meaning present on every in-sync replica.
- Consumers read only below the HW, so they never see data that a failover could erase. Transactional readers stop even earlier, at the Last Stable Offset.
- Followers report progress through fetch offsets, and learn the HW one fetch round later than the leader. That delay caused the old truncation bug.
- Leader epochs fixed truncation by letting a restarted follower ask the leader where its epoch ended instead of trusting a stale HW.
- Use replication factor 3, min.insync.replicas 2 and acks=all for important topics, and keep unclean leader election off unless availability clearly matters more.
- Monitor the ISR, not just broker uptime: under-replicated partitions, ISR shrink rates, replica lag and consumer lag reveal problems early.