Hotshard
Open the simulatorSimulator
Replication & durability inside the log

Kafka Internals: the replicated log

How a partition stays safe when a broker dies — replicas, the in-sync set, the high-water mark, and a clean leader failover.

A Kafka topic is split into partitions, and each partition is an append-only log: writes go on the end, reads walk forward by offset. The message-queue topic covers how producers and consumer groups share that log. This page is about the other half — the half that makes the log trustworthy. A single machine holding a partition can die and take your data with it, so Kafka keeps several copies on different brokers. The moment there's more than one copy, a hard question appears: when is a write actually safe to show a reader? Answer it too early and a crash can un-say a message a consumer already acted on. This page builds the machinery Kafka uses to answer it — replica followers, the in-sync replica set, the high-water mark that gates what's readable, and a leader failover that loses nothing — one piece at a time.

Open the simulator →~17 min read

Start here: the problem it solves#

TL;DRthe 30-second version
  • Each partition is a log kept on several brokers. One is the leader and takes all reads and writes; the others are followers that copy it.
  • A follower stays caught up by fetching new records from the leader and appending them. The set of replicas that are keeping up is the in-sync replica set, or ISR.
  • The high-water mark is the offset up to which every in-sync replica has the data. A consumer can only read below it, so it never sees a record that isn't fully replicated.
  • A slow follower holds the high-water mark back while it's still in the ISR. After a lag timeout the leader drops it from the ISR, and the mark advances again — on fewer copies.
  • When the leader dies, Kafka promotes an in-sync replica. Because that replica already had everything below the high-water mark, no acknowledged record is lost. The leader epoch bumps to fence the dead leader.

Picture a single partition living on one broker. Producers append to it, consumers read from it, and life is simple until that broker's disk fails. Now every record on that partition is gone, and every consumer that was reading it is stuck. For a log that other systems treat as the source of truth, that's not an acceptable failure. So Kafka keeps copies.

The number of copies is the replication factor. A partition with replication factor 3 lives on three brokers. One of them is the leader: it handles every produce and every fetch for that partition. The other two are followers, and their only job is to stay identical to the leader. If a broker dies, another copy is ready to take over, and no data is lost. That's the whole point of replication.

But copies create a subtler problem than they solve. Suppose the leader accepts a write and immediately tells the producer "got it." A microsecond later the leader's disk dies before either follower copied that record. The record is gone, yet the producer was told it succeeded, and a consumer might already have read it. You've lost data you promised to keep, and worse, a reader acted on a message that no longer exists. The question replication forces you to answer is exactly this one: when is a write safe enough to acknowledge and safe enough to read?

The trade-offReplication buys durability: a broker can die and the partition survives. In exchange you pay write latency (a record isn't safe until copies exist), you pay storage (three copies cost three times the disk), and you take on a new decision — how many copies must have a record before you call it done. That last decision is the whole subject of this page.

The idea that almost works: read whatever the leader has#

Start with the simplest rule. The leader holds the newest, most complete copy of the log, so let consumers read anything the leader has written. A record is appended at offset 5, and instantly a consumer can read offset 5. It's fast and it feels right — why hide a record that's already on disk?

Watch it break. The leader appends offset 5 and a consumer reads it. Before either follower has fetched offset 5, the leader crashes. A follower is promoted to leader, but its log only goes up to offset 4 — it never got 5. Now the new leader has no offset 5, so if that consumer asks for offset 6 next, the log has effectively rewound underneath it. A record that was read is now missing. This is a phantom read, and it happens precisely because the leader let a record be read before the other copies had it.

The naive rule made one specific mistake: it treated "the leader has it" as "it's safe." But the leader is a single machine, and a record that lives on only one machine is one crash away from gone. Safety can't mean "one copy has it." It has to mean "enough copies have it that losing one is survivable."

The moveDon't let a record be read until you know it survives a broker failure. Track how far each follower has copied, find the offset that every up-to-date replica has reached, and only allow reads below that line. That line is the high-water mark, and it's the heart of Kafka's durability model.

The mechanism: followers, the ISR, and the high-water mark#

Every partition has one leader and a set of followers. The leader owns the log. Each follower runs a simple loop: ask the leader for records after the offset I last have, append what comes back, repeat. This is a pull model — followers fetch from the leader rather than the leader pushing to them — and it's the same fetch path consumers use, which keeps the system simple.

To talk about progress you need one number per replica: the log-end offset, or LEO. A replica's LEO is the offset it will write next, so it's just how many records that replica currently holds. The leader's LEO is always the furthest along, because the leader writes first. A follower's LEO trails the leader's by however far behind it is.

Now define the set that matters. The in-sync replica set, the ISR, is the group of replicas that are keeping up with the leader — the leader itself plus every follower that has fetched recently enough. A follower is in-sync as long as it has caught up to the leader within a time window; fall behind that window and you're out. The ISR is the set of copies Kafka currently trusts to hold the data.

The high-water mark is the payoff. It is the smallest LEO across the current ISR — in other words, the offset that every in-sync replica has reached. Records below the high-water mark are committed: every trusted copy has them, so any single broker can die without losing them. A consumer is only ever allowed to read below the high-water mark. That single rule is what makes a read durable. The leader may hold records above the mark, but they're written, not yet safe, and no consumer can see them.

appendleader LEO 5→6b1 LEO 5b2 LEO 5HW 5
b1 fetchesleader LEO 6b1 LEO 5→6b2 LEO 5HW 5
b2 fetchesleader LEO 6b1 LEO 6b2 LEO 5→6HW 5
commitleader LEO 6b1 LEO 6b2 LEO 6HW 6
One record's journey from written to readable

The simulator runs exactly this. Load the first scenario, produce one record, and watch the leader append it while the high-water mark stays put. Each follower fetches in turn, and only on the final step — when every in-sync replica has the record — does the high-water mark advance and the record become readable. The gap between "the leader has it" and "you can read it" is the durability guarantee made visible.

This is also what a producer's acks setting controls. With acks=all, the leader waits to acknowledge a produce until the high-water mark has reached it, meaning the whole ISR has the record. With acks=1 the leader replies as soon as it alone has written the record, and with acks=0 it replies before even that. The three settings are a dial from fastest-and-riskiest to slowest-and-safest, and acks=all is the setting that actually gives you the guarantee this page is about.

PredictThe leader is at LEO 10. Followers b1 and b2 are both at LEO 8, and both are in the ISR. What is the high-water mark, and how many records can a consumer read?

Hint: The high-water mark tracks the slowest in-sync replica, not the leader.

The high-water mark is 8, so a consumer can read offsets 0 through 7 — eight records. The high-water mark is the smallest LEO in the ISR, and both followers are stuck at 8, so offsets 8 and 9 are on the leader but not yet on every in-sync replica. They stay unreadable until the followers fetch them, even though the leader wrote them.

When a follower falls behind#

The high-water mark waits for the slowest in-sync replica, and that cuts both ways. As long as every follower keeps up, the mark tracks the leader closely and writes commit quickly. But the day a follower slows down — a busy disk, a saturated network link, a long garbage-collection pause — it stops fetching, its LEO freezes, and because it's still in the ISR, it drags the high-water mark down with it.

Follow the consequence. The leader keeps appending, so its LEO climbs. The healthy follower keeps up. But the slow follower is frozen, and the high-water mark is the smallest LEO in the ISR, so the mark is pinned at the slow follower's frozen offset. New records pile up above the mark, written but unreadable. If producers are using acks=all, their writes now block, because the high-water mark isn't reaching the records they sent. One sick follower has stalled the whole partition's commits.

Kafka's answer is to stop waiting for a replica that isn't keeping up. Each follower has to catch up to the leader within a lag window — the replica.lag.time.max.ms setting, thirty seconds by default. A follower that falls behind that window is removed from the ISR by the leader. Once it's out, it no longer counts toward the high-water mark, so the mark is recomputed over the replicas that are keeping up and jumps forward. The backed-up records become readable, and acks=all producers unblock.

That jump forward has a cost, and it's worth saying plainly. The records that just became readable now live on fewer copies than before — the ISR shrank. Kafka let you keep making progress by lowering how many copies a committed record is guaranteed to be on. That's a deliberate trade of durability breadth for availability, and it's exactly the trade you tune with min.insync.replicas: if the ISR would shrink below that floor, acks=all writes are rejected instead, so you fail the write rather than commit it to too few copies.

Why the sim's mark stalls, then jumpsIn the second scenario, a follower stops fetching and stays in the ISR. Produce a record and the high-water mark holds still — the frozen follower is pinning it. Then shrink the ISR to evict the laggard, and the mark jumps to include the new record. You've watched availability and durability trade against each other in two steps.

When the leader dies: a clean handover#

Everything so far builds to the moment the leader itself fails. This is where the high-water mark earns its keep. When the leader broker goes down, the partition needs a new leader, and the rule for choosing one is strict: pick a replica from the ISR. An in-sync replica, by definition, has every record up to the high-water mark — every committed record. Promote one of those and no acknowledged data is lost. The controller, the component that manages leadership, does exactly this.

There's a second half to the handover, and it prevents a nasty failure. Imagine the old leader wasn't really dead — it was unreachable for a moment, and it comes back still believing it's the leader. If it kept accepting writes, you'd have two leaders diverging, which is a split brain. Kafka blocks this with the leader epoch: a version number for leadership that increases by one every time a new leader is elected. Every record is stamped with the epoch it was written under, and followers reject any request carrying an old epoch. When the recovered old leader tries to act, its epoch is stale, so it's refused. It's fenced.

The recovered broker then becomes a follower, discovers the higher epoch, and reconciles its log with the new leader — truncating any records it had written past the point the new leader agrees with, then fetching forward again. The epoch is also what lets it find that divergence point precisely. The result is one authoritative log, no lost committed records, and no two brokers accepting conflicting writes.

The third scenario runs the whole failover. Seed a few committed records so every replica is in-sync, then fail the leader. Watch the controller promote an in-sync follower, the leader epoch tick up by one, and the committed records survive intact on the new leader. The narration names the fence: the old leader's writes now carry a stale epoch and would be rejected.

Go deeperDeeper: unclean leader election, and why it's off by default

What if the leader dies and no in-sync replica is available — every follower had already fallen out of the ISR? Kafka's default (unclean.leader.election.enable=false) is to leave the partition offline until an in-sync replica returns. The partition stops rather than promote a replica that's missing committed records. It chooses consistency over availability.

Set unclean leader election to true and Kafka will promote an out-of-sync replica to keep the partition available, at the cost of silently dropping the records that replica never received. That's a genuine data-loss window, taken deliberately in exchange for staying up. Most durable pipelines leave it off; a few availability-first use cases turn it on with eyes open. It's the CAP trade-off, made a config flag.

What replication costs#

Durability isn't free, and the bill comes in three parts. The first is latency. With acks=all a produce isn't acknowledged until the high-water mark reaches it, which means at least one round trip to the slowest in-sync follower. That's the price of knowing the record survives a broker loss, and it's why acks=1 exists for workloads that would rather be fast than certain.

The second cost is storage and bandwidth. Replication factor 3 means every record is written three times, on three brokers, and shipped across the network to two followers. Triple the disk, and replication traffic that competes with client traffic for the same network. You size a Kafka cluster for the replicated write rate, not the raw produce rate.

The third cost is the tuning surface. Replication factor sets how many copies exist. The min.insync.replicas floor sets how many must acknowledge a write for acks=all to succeed. The lag window sets how patient the leader is before evicting a slow follower. A common durable configuration is replication factor 3 with min.insync.replicas 2: you keep three copies, require at least two to have a record before it commits, and can therefore lose any one broker without either losing data or blocking writes. Push min.insync.replicas to 3 and a single slow broker blocks every write; drop it to 1 and you're back to the phantom-read risk from the start of this page.

SettingWhat it controlsTypical durable value
replication factorHow many copies of each partition exist3
min.insync.replicasHow many in-sync copies must have a record before acks=all commits it2
acks (producer)How many acknowledgements the producer waits forall
replica.lag.time.max.msHow long a follower can lag before it's evicted from the ISR30000 (30s)
unclean.leader.election.enableMay an out-of-sync replica become leader (risking data loss)?false

The trades you're actually making#

Each knob hands you one thing and quietly takes another. Naming the trade for each is what turns these settings from magic numbers into decisions.

The high-water mark trades latency for safety. By refusing to show a record until every in-sync replica has it, you guarantee a reader never sees data that a crash could erase. The cost is that a read-ready record waits for the slowest in-sync follower, so a lagging replica delays commits for everyone. You get durability and you give up speed.

Evicting a slow follower trades durability breadth for availability. Dropping a laggard from the ISR lets the high-water mark advance again, so writes keep flowing. But the records that commit while the ISR is shrunk live on fewer copies, so a second failure is now closer to losing data. You get progress and you give up some safety margin.

Requiring an in-sync leader trades availability for correctness. Insisting that only an ISR member can be promoted means a failover never loses committed data. The cost is that if no in-sync replica survives, the partition goes offline rather than serve a stale one. You get a log you can trust and you give up staying up in the worst case — unless you flip unclean leader election and make the opposite trade on purpose.

How this compares to other replication schemes#

Kafka's model is leader-based replication with a dynamic in-sync set, and it sits between two more familiar schemes. It helps to place it next to them.

SchemeWhen a write is safeOn leader failureFeel
Async primary-backupAs soon as the primary has itMay lose recent writesFast, can lose data
Kafka ISR + high-water markWhen the whole in-sync set has itPromote an in-sync replica, lose nothing committedTunable, durable
Quorum / Raft (majority)When a majority has itElect a leader with the newest committed logDurable, fixed majority

The sharpest contrast is with a quorum system like Raft, which commits a record once a fixed majority — say two of three — has it. Kafka instead commits once the current in-sync set has it, and that set can shrink or grow. With min.insync.replicas set to 2 out of 3, the two schemes look almost identical in the common case. The difference is what happens under stress: a quorum always needs a majority, while Kafka can shrink its ISR to one replica and keep committing (giving up durability to stay available), or refuse to shrink past your floor and block instead. Kafka trades the rigidity of a fixed quorum for a knob you set per topic.

The consensus that actually elects leaders and tracks the ISR lives one layer down, in the controller. Classic Kafka kept that metadata in ZooKeeper; modern Kafka runs a built-in Raft-based controller quorum called KRaft. Either way, the partition data itself uses the leader-plus-ISR model on this page, and consensus is reserved for the small, critical job of agreeing who the leader is.

How this shows up in practice

The famous durability incident is the mirror image of this design. A well-known outage came down to running with acks=1 and unclean leader election enabled: a leader acknowledged writes its followers hadn't copied, then failed, and an out-of-sync replica was promoted, dropping the un-replicated records. The fix is the durable configuration this page describes — acks=all, min.insync.replicas 2, unclean election off — which is why those settings are the standard recommendation for data you can't lose.

The same shape appears far beyond Kafka. A replicated SQL database with a synchronous replica commits a transaction only once the replica confirms it — that's a high-water mark by another name. MongoDB replica sets use a write concern of majority and a term number that increases on each election to fence a stale primary, which is the leader epoch under a different label. Once you can see the in-sync set and the commit boundary, you start noticing them in every replicated store you meet.

Operationally, the metric teams watch most is under-replicated partitions: partitions whose ISR is smaller than their replication factor. A cluster showing many of them is telling you followers can't keep up — slow disks, a network bottleneck, or an overloaded broker — which means your effective durability is lower than the replication factor suggests, and a single failure is closer to data loss than you think.

Pitfalls
  • Using acks=1 for data you can't lose. The leader acknowledges before the followers copy the record, so a leader crash in that window loses an acknowledged write. Use acks=all for durability.
  • Leaving min.insync.replicas at 1 with replication factor 3. The ISR can silently shrink to just the leader and still commit, so a single failure loses data. Set the floor to 2 so a committed record is always on at least two copies.
  • Enabling unclean leader election without meaning to. It lets an out-of-sync replica become leader and silently drop committed records. Keep it off unless you have chosen availability over durability on purpose.
  • Confusing log-end offset with the high-water mark. The leader's LEO is how far it has written; the high-water mark is how far is readable. A large gap between them means followers are lagging.
  • Setting min.insync.replicas equal to the replication factor. Now every single replica must be up for writes to succeed, so one slow or dead broker blocks the whole partition. Leave headroom (2 of 3, not 3 of 3).
  • Ignoring under-replicated partitions. A shrunk ISR is a quiet warning that your real durability is below the replication factor. Treat a sustained non-zero count as an incident, not a curiosity.
In an interview

When a design leans on Kafka for durability, show that you know where the guarantee actually comes from. Say that each partition is replicated with a leader and followers, that the in-sync replica set is the group of copies keeping up, and that the high-water mark — the smallest log-end offset across the ISR — is the line below which a record is committed and readable. That three-sentence chain is the core of the topic and it signals you understand the mechanism, not just the vocabulary.

Then reach straight for the durable configuration and justify it: acks=all so a producer waits for the whole in-sync set, replication factor 3 with min.insync.replicas 2 so any one broker can fail without losing data or blocking writes, and unclean leader election off so a failover never promotes a replica that's missing committed records. Being able to name those four settings together, and say why each is set that way, is the senior signal here.

If the interviewer pushes on failure, walk the failover: the leader dies, the controller promotes an in-sync replica that already has every committed record, and the leader epoch bumps to fence the old leader so a zombie can't cause a split brain. If they push on the slow-follower case, explain that a laggard stalls the high-water mark until the lag timeout evicts it from the ISR, and that this trades durability breadth for availability — the exact tension min.insync.replicas lets you tune.

What's the difference between the log-end offset and the high-water mark?

The log-end offset (LEO) is the next offset a replica will write — how far it has appended. The high-water mark is the smallest LEO across the in-sync replica set — how far every trusted copy has reached. Consumers read below the high-water mark, never up to the leader's raw LEO.

Does a bigger ISR mean safer writes?

Up to a point. A larger in-sync set means a committed record is on more copies, which is more durable. But requiring more copies to acknowledge also means one slow copy can stall writes. min.insync.replicas is where you choose the balance; 2 out of 3 is the usual sweet spot.

How is this different from Raft or a quorum?

A quorum commits when a fixed majority has the record and always needs that majority. Kafka commits when its current in-sync set has it, and that set can shrink or grow. Kafka trades the rigidity of a fixed majority for a per-topic knob (min.insync.replicas), and pushes the actual leader-election consensus down into the controller.

What does the leader epoch actually do?

It's a leadership version number that increases on every election. Records are stamped with it, and followers reject requests carrying an old epoch. That fences a recovered old leader so it can't keep writing, and it gives followers a precise point to reconcile their logs after a failover.

References
References

Feedback on this topic →