A clean election picks the new leader from the in-sync replicas, so the new leader holds every record the old one had committed. That rule has a gap. The ISR can shrink to the leader alone when every follower falls behind, and if that leader then dies, no replica is in sync with it. The controller has 2 options: leave the partition without a leader until the dead broker returns, or make a replica that is missing records the leader. The setting that picks between them is unclean.leader.election.enable.
A partition with one replica left in sync
Take partition 3 of orders, replicated on brokers 0, 1 and 2 with broker 0 leading. Both followers sit on a rack whose switch starts dropping packets, their fetches stall, and after replica.lag.time.max.ms the leader removes them from the ISR. Writes keep landing: min.insync.replicas is 1, the ISR is {0}, and the high watermark advances with every record. The log end offsets drift apart.
| Broker | Role | Log end offset | In the ISR |
|---|---|---|---|
| 0 | leader | 100 | yes |
| 1 | follower | 61 | no |
| 2 | follower | 58 | no |
Off: wait for the leader
With the setting at its default of false, the controller leaves partition 3 with no leader. Writes to it fail, readers on it get nothing, and the other partitions of orders carry on. When broker 0 comes back with its log intact, it resumes as leader at offset 100, the followers catch up, and every acknowledged record is still there. The price is that this partition stays offline for as long as broker 0 is down.
On: promote a stale replica
With the setting at true, the controller makes a replica from outside the ISR the leader. It never sees the replicas' log end offsets, so it does not look for the freshest one. It takes the first live broker in the partition's replica list, here [0, 1, 2], which is broker 1. Broker 1 leads from its own log end, offset 61, and the ISR becomes {1}. Records 61 through 99, all acknowledged, now exist only on broker 0's powered-off disk. Had the list been [0, 2, 1], broker 2 would lead from offset 58 and records 58 to 99 would be lost, 3 more than here, while broker 1 sat holding them. New writes arrive at once and the leader assigns them offsets starting at 61. Offset 80 now names a record written after the failure, and any reader that already consumed the old record 80 holds a value the cluster no longer has.
The old leader truncates on return
| Moment | Broker 0 log | Broker 1 log | Readable range |
|---|---|---|---|
| before the crash | 0 to 99 | 0 to 60 | 0 to 99 |
| after the unclean election | offline | 0 to 60 | 0 to 60 |
| after 5 new writes | offline | 0 to 65 | 0 to 65 |
| broker 0 rejoins | 0 to 65, truncated | 0 to 65 | 0 to 65 |
- The partition stays offline until a replica with the full log returns
- Every acknowledged record survives
- Writers and readers of that partition wait
- A new leader is chosen within seconds
- Records only the dead leader held are dropped
- Offsets past the divergence point are reassigned to new records
Choosing per topic
The setting is a topic config and overrides the broker default. A topic of page-view counts can run with it on, since a gap of 40 records costs a dashboard a blip. A topic of payments runs with it off and accepts the outage. You can also leave it off everywhere and trigger a single unclean election by hand when the wait has gone on too long: kafka-leader-election.sh --bootstrap-server b1:9092 --election-type UNCLEAN --topic orders --partition 3. That choice is yours at the moment, rather than the controller's by rule.
min.insync.replicas at 2, writers using acks=all are refused while the ISR is that small, so their at-risk records were never acknowledged. A writer using acks=1 is acknowledged by the leader alone, so its records can still be acknowledged and then lost.The roles a node can declare
Each node declares process.roles: controller, broker, or broker,controller for both on one process. Controllers form the quorum; brokers hold topic partitions. Running both on 1 process suits a laptop, and a production cluster keeps them on separate machines so that a broker drowning in fetches never delays an election.
A broker's file names the same bootstrap list under process.roles=broker, which is how it finds the quorum. Each node is formatted once with kafka-storage.sh format, which stamps the cluster id into its log directory, and a controller formatted with --initial-controllers joins as a voter from the start.
The metadata log
The quorum's state is a topic named __cluster_metadata with 1 partition, held by every controller. One controller is its leader, called the active controller, and the others replicate it as followers, with the same fetch loop a topic partition uses. A record in it counts as committed once a majority of the voters have written it: 2 of 3, or 3 of 5. Each record describes one change: a topic created, a partition's ISR shrinking, a broker registering, a leader changing. Replaying the log from the start rebuilds the whole cluster's metadata, which is what a controller does when it boots, and it snapshots the log periodically so that the replay stays short.
| Record | What it says |
|---|---|
| RegisterBrokerRecord | broker 4 at b4:9092, rack us-east-1b, fenced |
| TopicRecord | topic orders, id 7f3e |
| PartitionRecord | orders-3 replicas [0,1,2] isr [0,1,2] leader 0 epoch 12 |
| PartitionChangeRecord | orders-3 isr [0] leader 0 epoch 13 |
| PartitionChangeRecord | orders-3 isr [1] leader 1 epoch 14 |
Fencing a dead broker
Each broker sends a heartbeat to the active controller on a fixed interval, and when the controller has heard nothing for longer than the session timeout it fences the broker: it writes a record marking it fenced, then one PartitionChangeRecord per partition that broker led, naming a new leader from each ISR. Those records commit once a majority of controllers hold them. Each broker learns by fetching: it runs a fetch against the metadata log, exactly as a follower fetches a partition, and applies every new record it reads. A broker finds out it has become leader of orders-3 by reading that fact from the log.
A dead broker's silence reaches the active controller, turns into records in the metadata log, and reaches every broker through its own fetch.
Watching the quorum
kafka-metadata-quorum.sh --bootstrap-server b1:9092 describe --status prints the active controller and how far each voter has replicated:
| Field | Value |
|---|---|
| LeaderId | 2 |
| LeaderEpoch | 31 |
| HighWatermark | 48211 |
| MaxFollowerLag | 0 |
| CurrentVoters | [1, 2, 3] |
| CurrentObservers | [10, 11, 12] |
- ▸Format the new controller with the cluster id, start it, then run kafka-metadata-quorum.sh add-controller on it to turn it from observer into voter
- ▸Before running kafka-metadata-quorum.sh remove-controller, check with describe --replication that the voters staying are caught up, and prefer removing one that is not the active controller
- ▸Add or remove 1 voter at a time so a majority is always well defined
- ▸Keep an odd count: 4 voters survive the same single loss as 3 and need 3 to agree instead of 2
The reassignment tool
kafka-reassign-partitions.sh runs in 3 steps. --generate takes a list of topics and a --broker-list and proposes a spread, --execute submits a reassignment file, and --verify reports which partitions have finished and clears the throttles it set. The file names the target replica set per partition:
The first entry of replicas is the preferred leader, so the file also decides who leads once the move finishes. log_dirs can name a specific disk on the broker instead of any, which is how a partition is moved from a full disk to an empty one on the same machine.
The controller's steps during a move
For orders-3, moving from [0, 1, 2] to [1, 2, 4], the controller records broker 4 as an adding replica and broker 0 as a removing replica. The partition's replica set becomes [0, 1, 2, 4] for the duration. Broker 4 opens a fetch against the leader at the leader's log start offset and copies every segment, while the ISR stays {0, 1, 2} and writes are unaffected. When broker 4 is caught up, the leader adds it to the ISR. The controller then drops broker 0 from the replica set and the ISR, moves leadership to broker 1 because broker 0 was leading, and broker 0 deletes its copy. While broker 4 copies and sits outside the ISR, no write waits for it. Once it joins the ISR it counts like any other in-sync replica, and a write that is acknowledged only when every in-sync replica has it waits for broker 4 too.
| Moment | Replicas | ISR | Leader |
|---|---|---|---|
| before execute | [0, 1, 2] | {0, 1, 2} | 0 |
| copying | [0, 1, 2, 4] | {0, 1, 2} | 0 |
| broker 4 caught up | [0, 1, 2, 4] | {0, 1, 2, 4} | 0 |
| done | [1, 2, 4] | {1, 2, 4} | 1 |
The cost of a move
A moving replica reads the whole partition over the network and writes it to a new disk. A 400 GB partition is 400 GB of reads from the leader's disk, 400 GB across the network and 400 GB of writes, on top of the traffic the topic already carries. Unthrottled, that copy competes with the leader's followers and with the writers sending to it. The tool takes --throttle in bytes per second, which sets leader.replication.throttled.rate and follower.replication.throttled.rate on the brokers involved and lists only the moving replicas as throttled on the topic, so in-sync followers are never slowed.
| Partition size | Throttle | Copy time |
|---|---|---|
| 400 GB | 50 MB/s | 2 h 13 min |
| 400 GB | 200 MB/s | 33 min |
| 2 TB | 100 MB/s | 5 h 33 min |
The copy must also outrun the writers. A partition taking in 20 MB/s under a 50 MB/s throttle closes the gap at 30 MB/s, and a throttle below the write rate never finishes. Running --verify after the move clears the throttle settings. Skipping it leaves the throttled-replica lists on the topic and the rate settings on the brokers in place, so replication of those replicas stays capped at the old rate, and any later move that reuses them crawls at that rate too. On a topic with tiered storage, the new replica copies only the local segments and reads the remote ones from the object store when asked, which makes the move as cheap as the local retention.
Changing your mind
A move in flight can be stopped with --cancel, which removes the adding replicas and leaves the originals in place; the copied bytes are discarded. Submitting a second file for a partition already moving replaces its target. A move is also how a replication factor is raised, by naming 4 brokers where there were 3, or lowered, by naming 2.
- Move a few partitions at a time and watch progress with verify
- Pick a throttle above the partition's write rate
- Run verify once every partition reports complete, to clear the throttles
- Generate a plan first and read it before executing
- Move every partition in one file on a busy cluster
- Start a move without a throttle on a leader already near its disk limit
- Forget that the new replica copies every retained byte from the start
- Assume a new broker fills up on its own
Replication protects a partition against a broker dying. It does nothing when the whole cluster goes: a region's power, a bad configuration push, a disk corruption that every replica copied. For that, a second cluster in another place holds a copy of the topic, written by a mirror. Apache Kafka ships one as MirrorMaker 2, started with connect-mirror-maker.sh and a properties file.
A reader on one side, a writer on the other
| Setting in mm2.properties | Value | What it does |
|---|---|---|
| clusters | east,west | names the 2 clusters by alias |
| east.bootstrap.servers | east-b1:9092,east-b2:9092 | where the mirror reaches east |
| west.bootstrap.servers | west-b1:9092,west-b2:9092 | where the mirror reaches west |
| east->west.enabled | true | turns on the flow from east to west |
| east->west.topics | orders,payments | the topics that flow copies |
| replication.factor | 3 | replication factor of the topics it creates on west |
| sync.topic.configs.enabled | true | copies topic configs such as retention |
| offset.lag.max | 100 | how many records apart offset syncs are written |
| offset-syncs.topic.location | target | keeps the offset syncs on west |
Topic names and offsets on the target
Topic orders on east appears on west as east.orders, with the same partition count and each record in the same partition number it had at the source. The prefix is the source cluster's alias and does 2 jobs: a reader on west can tell local records from mirrored ones, and a mirror running the other way, west to east, skips any topic whose prefix names east, so 2 clusters can mirror each other without a record circling forever. Topic configs such as retention are copied too, so the mirror's log is cleaned by the same rules as the source.
| offset | key | value |
|---|---|---|
| 1200 | c-41 | order 9001 |
| 1201 | c-17 | order 9002 |
| 1202 | c-41 | order 9003 |
| 1203 | c-08 | order 9004 |
| offset | key | value |
|---|---|---|
| 877 | c-41 | order 9001 |
| 878 | c-17 | order 9002 |
| 879 | c-41 | order 9003 |
Failing a reader over
A reader of orders on east keeps its position as a source offset, say 1202. On west that number means nothing, since offset 1202 of east.orders is a different record, if it exists at all. The mirror solves this by writing offset syncs: a record, on a topic of its own named mm2-offset-syncs.west.internal, pairing a source offset with the target offset the same record received, written whenever the pair has moved by offset.lag.max records. That topic lives on the source cluster by default, which is useless when the source is the cluster that died, so a mirror built for failover sets offset-syncs.topic.location=target to keep it on west. To fail over, the reader finds the latest sync whose source offset is at or below its own, and starts from that sync's target offset.
| Sync | Source offset | Target offset |
|---|---|---|
| 1 | 1000 | 677 |
| 2 | 1100 | 777 |
| 3 | 1200 | 877 |
- ▸Its bootstrap servers: west's brokers instead of east's
- ▸Its topic name: east.orders, keeping the same partition number
- ▸Its starting offset: translated through the latest offset sync, never copied as is
- ▸Its tolerance: up to offset.lag.max records arrive a second time
- ▸Its expectations: records inside the mirror's lag at the moment east died are not there
Mirroring through an unclean election
Readers that keep their positions on the cluster itself can have the mirror translate them in bulk; that mechanism belongs to a later lesson on consumer groups.
Broker and reader settings
The broker needs replica.selector.class set to the rack-aware selector and a broker.rack naming its zone, which rack-aware placement already requires. The reader sets client.rack to the zone it runs in. On the reader's first fetch, the leader answers with no records and a preferred read replica, the follower whose rack matches; the reader sends its next fetches there.
How far behind a follower reads
A follower serves only up to its own high watermark, and it learns the leader's high watermark one fetch late: the leader advances it when every in-sync replica has fetched a record, and tells the followers in the response to their next fetch. A record committed at the leader at 10:00:00.000 becomes readable from the leader at once and from the follower after the follower's next round trip, which is bounded by replica.fetch.wait.max.ms, 500 ms by default, plus the network.
| Time | Leader HWM | Follower HWM | Reader in zone a | Reader in zone b |
|---|---|---|---|---|
| 10:00:00.000 | 500 | 500 | up to 499 | up to 499 |
| 10:00:00.120 record 500 fetched by followers | 500 | 500 | up to 499 | up to 499 |
| 10:00:00.121 leader advances | 501 | 500 | up to 500 | up to 499 |
| 10:00:00.260 follower's next fetch returns | 501 | 501 | up to 500 | up to 500 |
Two readers of the same partition in different zones therefore see each record at different moments, around 140 ms apart in this table and up to half a second in a quiet partition. They never see different records at an offset, and never a record the leader has not committed, because the follower's high watermark never passes the leader's. The order inside the partition is unchanged. What changes is that latest means something different for each reader: the end of the log as the follower knows it.
Falling back to the leader
The leader picks the follower only from the ISR, so a reader is never pointed at a replica that fell behind. If the chosen follower then drops out of the ISR or dies, the reader's fetch fails, it goes back to the leader, and the leader names a new preferred replica or serves the reader itself. Every metadata.max.age.ms, 5 minutes by default, the reader asks the leader again, which lets the leader move it after a reassignment puts a replica in its zone. If no replica shares the reader's rack, it reads from the leader as before. Writes are untouched: a follower never accepts a record, so the reader's zone decides where its reads go and nothing about where writes go.
| Setup, 3 zones, 100 MB/s of reads in each | Cross-zone read traffic | Cross-zone replication traffic |
|---|---|---|
| every reader fetches from the leader | 200 MB/s | 2 times the write rate |
| each reader fetches from its zone's replica | 0 MB/s | 2 times the write rate |
> Picture Uber's trip events during an evening rush in one region. A rack fails and takes the last in-sync replica of a busy partition with it; the controller quorum, down to 2 of 3 voters, still commits the decision to leave it leaderless because the topic refuses unclean election. Trip readers in the other 2 zones were fetching from local followers, so their last visible offset sits a fetch behind the dead leader's. Meanwhile the mirror in the second region trails by 3 seconds, and the on-call decides whether to fail over the pricing readers to it or wait 4 minutes for the rack.
Apache Kafka is an open-source distributed event streaming platform used by thousands of companies for high-performance data pipelines, streaming analytics, data integration, and mission-critical applications.
kafka.apache.org
- Unclean Leader Election
- unclean.leader.election.enableEligible Leader Replicas
- KRaft Controllers
- ControllersProcess Roles
- Moving Partitions
- Expanding your clusterAutomatically migrating data to new machines
- Mirroring to Another Cluster
- Geo-Replication OverviewWhat Are Replication Flows
- Reading the Nearest Replica
- replica.selector.classclient.rack
Apache Kafka, Kafka, and the Kafka logo are either registered trademarks or trademarks of The Apache Software Foundation in the United States and other countries.
What a cluster trades away for availability, where its metadata lives, and how partitions travel between brokers, racks and regions
- Category
- Kafka
- Difficulty
- advanced
- Duration
- 20 minutes
- Challenges
- 7 hands-on challenges
Topics covered: Unclean Leader Election, KRaft Controllers, Moving Partitions, Mirroring to Another Cluster, Reading the Nearest Replica
Lesson Sections
- Unclean Leader Election (concepts: In-sync replicas)
A partition with one replica left in sync At offset 100, broker 0 loses power and goes down. Its disk still holds offsets 0 to 99, but no one can read them until the machine boots again. The controller sees a dead leader and an ISR with nobody else in it. Off: wait for the leader On: promote a stale replica The old leader truncates on return Broker 0 restarts holding offsets 0 to 99 and finds broker 1 leading with a newer leader epoch. It asks the leader where their logs diverge, learns that the
- KRaft Controllers (concepts: Replication and leaders)
Every election in the last section was carried out by a controller: the one part of the cluster that holds the list of every partition's replicas, leader and ISR, and that writes down a new leader when a broker dies. In Apache Kafka 4.4 that controller runs as a small quorum of its own, using the Raft protocol, and keeps its metadata in a log that works like any partition you have read so far. The name for this is KRaft. The roles a node can declare The metadata log Fencing a dead broker Watchin
- Moving Partitions (concepts: Replication and leaders)
Adding broker 4 to a cluster of 3 changes nothing on its own. Every existing partition keeps its replica set, broker 4 holds nothing, and only partitions created after it joined can land there. To spread the load, you move partitions, which means telling the controller a new replica set for each one and letting the new replica copy the whole partition from the leader while the topic keeps serving. The reassignment tool The controller's steps during a move The cost of a move Changing your mind
- Mirroring to Another Cluster (concepts: Replication and leaders)
A reader on one side, a writer on the other The mirror is a client of both clusters. It reads records from the source cluster's partitions, as any reader does, and writes each to the target cluster, as any writer does, remembering how far it has read. Because it is an ordinary client, it is asynchronous: a record acknowledged on the source is on the target only after the mirror has fetched and written it, so the target trails by the mirror's lag. It also delivers at least once: if the mirror cra
- Reading the Nearest Replica (concepts: Replication and leaders)
By default every read of a partition goes to its leader, wherever it sits. With replicas spread across 3 zones, a leader in zone a serves readers in zones b and c across zone boundaries, which cloud providers bill per gigabyte and which adds milliseconds to each fetch. The replicas in b and c hold the same committed records and do nothing with them but wait. Since KIP-392, a reader can fetch from the follower in its own zone instead. Broker and reader settings How far behind a follower reads Fal