KRaft and the controller
Something must decide which broker leads each partition, which brokers are alive, and what every topic's settings are. In Kafka 4 that is a small group of controllers that keep the cluster's metadata in a log of its own.
Try it: a majority must be running
Click a controller to stop or start it. The cluster can change its metadata only while more than half of the controllers run.
What it is and why it exists
A Kafka cluster has metadata: the list of topics, the replicas and leader of every partition, every setting, and the list of brokers. All brokers must agree on it. If two brokers both believed they led the same partition, data would be lost.
Before Kafka 4, a separate system named ZooKeeper held the metadata. Kafka 4.0 removed it. Kafka now stores its metadata in itself, with a design named KRaft: Kafka plus Raft. Raft is a well-known method for several servers to agree on one ordered log, as long as more than half of them can talk to each other.
The servers that run it are controllers. They form the quorum, the voting group. One of them is elected leader and is the active controller. It makes every metadata decision and writes it as a record in the metadata log. The other controllers copy that log and are ready to take over. Brokers copy it too, and act on what they read.
The one-sentence version. The cluster's metadata is one more Kafka log, written by one elected controller, copied to a majority before it counts, and replayed by every broker.
Mental model
You already know the log from module 01 and replication from module 04. KRaft uses the same ideas with one change: for metadata, a record is committed when a majority of the controllers have it, not when an in-sync set has it.
Think of a committee of three that keeps the official minute book. One member writes. An entry is official when two of the three have it. If the writer leaves, the other two elect a new writer between them. If two members leave, the last one cannot make anything official, because one is not a majority of three.
| Controllers | Majority | Failures survived |
|---|---|---|
| 1 | 1 | 0 |
| 3 | 2 | 1 |
| 5 | 3 | 2 |
Four controllers survive one failure, the same as three, so an even number adds cost without adding safety. Production clusters use three, or five when two failures at once must be survived.
Two roles for a server
Each Kafka process has a process.roles setting.
broker: stores partitions and serves clients.controller: votes in the quorum and stores the metadata log.broker,controller: both in one process. This is combined mode. The lab cluster uses it because it needs fewer containers. For production, use separate controller servers, so that a busy or failing broker cannot disturb the quorum.
The metadata is a list of changes
The metadata log does not store "the current state". It stores each change. Any server can rebuild the current state by replaying the records from the start. Step through six records of the kinds found in the lab cluster's log. The third and fourth are copied from it; the others are shortened examples:
Step through it: replay the metadata log
| State after replay | Value |
|---|
How it works internally
The metadata log
The metadata log is a topic with one partition, named __cluster_metadata. It lives in the directory __cluster_metadata-0 on every controller and every broker. It has segments and indexes like any other log.
The record types tell you what the controller does. In the lab cluster's log, after 40 minutes of labs:
| Record type | Count | Meaning |
|---|---|---|
NO_OP_RECORD | 4,731 | Written every 500 ms when nothing else happens, so that every server can tell the log is alive. |
PARTITION_CHANGE_RECORD | 1,533 | A partition's leader or in-sync set changed. Each broker stop in the labs caused many. |
PARTITION_RECORD | 79 | A partition was created, with its replicas, in-sync set and leader. |
BROKER_REGISTRATION_CHANGE_RECORD | 51 | A broker was fenced or unfenced. |
CONFIG_RECORD | 23 | A topic or broker setting changed. |
TOPIC_RECORD | 19 | A topic was created. |
REGISTER_BROKER_RECORD | 13 | A broker started and registered. |
FEATURE_LEVEL_RECORD | 6 | The cluster's feature versions were set. |
Electing the active controller
- Every controller follows the current leader and fetches from it. Time is divided into epochs; each epoch has at most one leader.
- If a controller hears nothing from the leader for
controller.quorum.fetch.timeout.ms, 2 seconds, it first asks the others if they would vote for it. This check avoids needless elections. - If a majority would, it starts a new epoch and asks for real votes.
- A controller votes for a candidate only if the candidate's log is at least as complete as its own. So the winner always has every committed record.
- The candidate with votes from a majority becomes leader. In the lab this took about 1 second.
How brokers stay current
A broker is an observer of the quorum. It fetches the metadata log like a follower but does not vote. As it reads each record it updates its own picture: "I am now leader of orders-0", "the in-sync set of orders-1 changed". Clients then get this picture when they ask any broker for metadata.
The life of a broker
Step through it: from start to stop
meta.properties.Fenced means the controller does not let the broker lead any partition and does not count it in any in-sync set. A broker is fenced when it starts, until it has caught up. It is fenced again if the controller hears no heartbeat for broker.session.timeout.ms, 9 seconds. Fencing is how Kafka stops a broker that is alive but cut off from acting on old information.
Snapshots
Replaying every change since the cluster was created would take longer and longer. So each server writes a snapshot from time to time: the full state at one offset. After that, the older part of the log can be deleted. A new or restarted server loads the newest snapshot and replays only the records after it. A snapshot is written after 20 MiB of new records or after 1 hour, whichever comes first.
Identity on disk
Each data directory has a file meta.properties with the cluster id and the node id. A server refuses to start if these do not match its configuration. This stops a disk from one cluster being used in another by mistake. Before a first start, the directory is prepared with kafka-storage.sh format; the lab's Docker image does this for you. unverified: I did not run the format tool
Feature versions
Servers on a new software version do not use new record formats until the cluster's metadata.version is raised. This makes a rolling upgrade safe: upgrade every server first, then raise the version with kafka-features.sh as a separate step.
Every relevant config
All defaults below were read from the lab cluster with kafka-configs.sh --describe --all.
| Config | Default | What it does | When to change it |
|---|---|---|---|
process.roles | none; the lab sets broker,controller | What this server is: broker, controller, or both. | Use separate controllers in production. |
node.id | none (required) | The unique number of this server. | Set once. Never reuse a number for a different server. |
controller.quorum.voters | none; the lab sets three | A fixed list of the controllers: id, host and port for each. | The older, fixed way to define the quorum. All servers must have the same list. |
controller.quorum.bootstrap.servers | empty | Addresses used to find the quorum when the set of controllers can change. | The newer way. It allows adding and removing controllers without a full restart. unverified |
controller.listener.names | none; the lab sets CONTROLLER | Which network listener the controllers use. | Keep the controller listener on a private network. |
controller.quorum.election.timeout.ms | 1000 | How long a candidate waits for votes before it tries again. | Raise on slow or far-apart networks. |
controller.quorum.fetch.timeout.ms | 2000 | Time without contact with the leader before a controller starts an election. | Raise if elections happen with no real failure. |
controller.quorum.election.backoff.max.ms | 1000 | Longest random wait before a new election attempt, to stop two candidates from tying again and again. | Rarely. |
controller.quorum.request.timeout.ms | 2000 | Timeout for one request between controllers. | Rarely. |
broker.heartbeat.interval.ms | 2000 | How often a broker reports to the active controller. | Rarely. |
broker.session.timeout.ms | 9000 | Time without a heartbeat before a broker is fenced. | Lower it for faster failover of a hung broker. Too low fences healthy brokers during short pauses. |
metadata.log.dir | null (uses the first of log.dirs) | Where the metadata log is stored. | Put it on its own fast disk on a busy broker. |
metadata.log.max.record.bytes.between.snapshots | 20971520 (20 MiB) | Write a snapshot after this much new metadata. | Rarely. |
metadata.log.max.snapshot.interval.ms | 3600000 (1 hour) | Write a snapshot at least this often when there are new records. | Rarely. |
metadata.max.retention.bytes, metadata.max.retention.ms | 104857600 (100 MiB), 604800000 (7 days) | How much of the metadata log and its snapshots to keep. | Rarely. |
metadata.log.segment.bytes | 1073741824 (1 GiB) | Segment size of the metadata log. | Rarely. |
metadata.max.idle.interval.ms | 500 | The active controller writes a no-op record this often when idle. | Rarely. |
controller.quorum.auto.join.enable | false | Lets a new controller add itself to the quorum. | Only with the newer, changeable quorum. unverified |
Failure scenarios
The active controller fails
- What happens
- The other controllers elect a new leader. In the lab: about 1 second, and the leader epoch went from 3 to 4.
- You observe
- A new
LeaderIdinkafka-metadata-quorum.sh describe --status. Clients notice nothing: they do not talk to the controller. - Risk
- Small. While the election runs, no partition can change leader, so a broker failure in that same second waits a little longer.
The quorum loses its majority
- What breaks
- Nothing in the metadata can change: no topic creation, no leader election, no in-sync set change, no setting change.
- You observe
- In the lab, with two of three nodes stopped,
kafka-topics.sh --describeand--createboth hung until the timeout. The remaining node logged "Election timed out before receiving sufficient vote responses". - What still works
- Brokers keep serving the partitions they already lead, with the metadata they already have. A leader that fails in this state is not replaced. unverified: the lab did not test client traffic in this state
- Fix
- Bring controllers back until a majority runs. Nothing else helps. Do not delete controller data to "reset" it.
A broker is fenced but still running
- What breaks
- The broker lost contact with the controller for 9 seconds, for example through a network fault or a long pause. The controller moved its leaderships away.
- You observe
isFenced: truefor that broker inkafka-broker-api-versions.sh. Its partitions show other leaders and a smaller in-sync set.- Fix
- When contact returns, the broker catches up on metadata and is unfenced by itself. If it happens often, look for long pauses or network loss before you raise the timeout.
A controller's disk is lost
- What breaks
- That controller has no metadata log and no identity file.
- Risk
- If it rejoins with an empty disk and the same id, it could vote as if it had never promised anything. With a fixed voter list this needs great care.
- Fix
- Follow the documented replacement procedure for your quorum type. With the newer, changeable quorum, each controller disk has its own directory id, and a replaced disk is treated as a new voter. unverified
Combined mode under load
- What breaks
- A broker that is also a controller becomes slow because of client traffic. Its controller part misses fetch deadlines, and the quorum holds elections with no real failure.
- You observe
- The quorum's leader epoch rises often.
- Fix
- Separate the roles: three small servers that are controllers only.
Too many partition changes at once
- What breaks
- After a broker fails, the controller must write a change record for each partition it led or followed. On a cluster with hundreds of thousands of partitions this takes time, and failover is slower.
- Evidence
- The small lab cluster already wrote 1,533 partition change records for a handful of broker stops.
- Fix
- Keep the partition count for each broker within tested limits. This is one reason not to create partitions "just in case".
Hands-on lab
Every command below was run on 4 October 2026 against the lab cluster. Node 1 was the active controller at the start. Yours may differ; read LeaderId first and adjust.
k() { docker exec -i kafka-1 /opt/kafka/bin/"$@"; }
B=kafka-1:19092Step 1. Look at the quorum
k kafka-metadata-quorum.sh --bootstrap-server $B describe --status
k kafka-metadata-quorum.sh --bootstrap-server $B describe --replicationClusterId: 4L6g3nShT-eMCtK--X86sw
LeaderId: 1
LeaderEpoch: 3
HighWatermark: 5717
MaxFollowerLag: 0
MaxFollowerLagTimeMs: 0
CurrentVoters: [{"id": 1, …"CONTROLLER://kafka-1:9093"}, {"id": 2, …}, {"id": 3, …}]
CurrentObservers: []
NodeId LogEndOffset Lag Status
1 5719 0 Leader
2 5719 0 Follower
3 5719 0 Follower
Node 1 is the active controller. The metadata log has about 5,700 records, and both followers have all of them. HighWatermark means the same as in module 04, with "majority" in place of "in-sync set".
Step 2. Read the identity files
docker exec kafka-1 cat /tmp/kraft-combined-logs/meta.properties
docker exec kafka-1 cat /tmp/kraft-combined-logs/__cluster_metadata-0/quorum-statecluster.id=4L6g3nShT-eMCtK--X86sw
directory.id=3uiEyBOPVNJZjvqFZCMIdA
node.id=1
version=1
{"clusterId":"","leaderId":1,"leaderEpoch":3,"votedId":1,
"currentVoters":[{"voterId":1},{"voterId":2},{"voterId":3}],"data_version":0}
The second file is this node's memory of the election: the current epoch, the leader, and whom it voted for. It is written to disk so that a restart cannot make the node vote twice in one epoch.
Step 3. Read the metadata log
The flag is --cluster-metadata-decoder. Count the record types, then look at single records.
dump() { docker exec kafka-2 /opt/kafka/bin/kafka-dump-log.sh --cluster-metadata-decoder \
--files /tmp/kraft-combined-logs/__cluster_metadata-0/00000000000000000000.log; }
dump | grep -o '"type":"[A-Z_]*"' | sort | uniq -c | sort -rn | head
dump | grep -m1 'TOPIC_RECORD' | grep -o 'payload: .*'
dump | grep -m1 '"PARTITION_RECORD"' | grep -o 'payload: .*'
dump | grep -m1 'PARTITION_CHANGE_RECORD' | grep -o 'payload: .*'4731 "type":"NO_OP_RECORD"
1533 "type":"PARTITION_CHANGE_RECORD"
79 "type":"PARTITION_RECORD"
51 "type":"BROKER_REGISTRATION_CHANGE_RECORD"
23 "type":"CONFIG_RECORD"
19 "type":"TOPIC_RECORD"
payload: {"type":"TOPIC_RECORD","version":0,"data":{"name":"orders","topicId":"mocIQ-cjTlOVeEC3U3NxRQ"}}
payload: {"type":"PARTITION_RECORD","version":2,"data":{"partitionId":0,"topicId":"mocIQ-cjTlOVeEC3U3NxRQ",
"replicas":[2,3,1],"isr":[2,3,1],"removingReplicas":[],"addingReplicas":[],"leader":2,"leaderEpoch":0,
"partitionEpoch":0,"directories":[…]}}
payload: {"type":"PARTITION_CHANGE_RECORD","version":2,"data":{"partitionId":0,"topicId":"dEFhkuV0…","isr":[2,1]}}
This is the orders topic from module 01, as the controller recorded it: first the topic, then each partition with its replicas and leader. The last record is an in-sync set that shrank to 2 and 1 when broker 3 was stopped in an earlier lab. A change record holds only the fields that changed.
Step 4. Stop the active controller
Node 1 is the leader, so run the tools from node 2.
docker stop kafka-1
docker exec -i kafka-2 /opt/kafka/bin/kafka-metadata-quorum.sh --bootstrap-server kafka-2:19092 describe --status
docker exec -i kafka-2 /opt/kafka/bin/kafka-metadata-quorum.sh --bootstrap-server kafka-2:19092 describe --replication
docker exec -i kafka-2 /opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka-2:19092 \
--create --topic during-failover --partitions 1 --replication-factor 2LeaderId: 2 LeaderEpoch: 4 ← about 1 second after the stop NodeId LogEndOffset Lag Status 2 5875 0 Leader 1 -1 5876 Follower 3 5875 0 Follower Created topic during-failover.
Node 2 is the new active controller, in a new epoch. Two of three controllers run, which is a majority, so the cluster accepted a new topic.
Step 5. Lose the majority
docker stop kafka-3
docker exec -i kafka-2 timeout 12 /opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka-2:19092 \
--describe --topic during-failover
docker exec -i kafka-2 timeout 14 /opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka-2:19092 \
--create --topic no-quorum --partitions 1 --replication-factor 1
docker logs --since 20s kafka-2 2>&1 | grep -i 'election' | tail -1(no output: both commands ran into the time limit) INFO [RaftManager id=2] Election timed out before receiving sufficient vote responses to become candidate. …
One controller of three cannot elect itself. It keeps asking for votes that nobody answers. Even reading a topic's description did not work, because the tool asks the cluster for current metadata.
Step 6. Restore
docker start kafka-1 kafka-3
# wait 20 seconds
k kafka-metadata-quorum.sh --bootstrap-server $B describe --statusLeaderId: 2 LeaderEpoch: 6 MaxFollowerLag: 0
Node 2 is leader again, now in epoch 6: the failed election attempts each used an epoch. The two returning nodes copied the records they had missed.
Step 7. See which brokers are registered
k kafka-broker-api-versions.sh --bootstrap-server $B | grep '^kafka-'
k kafka-features.sh --bootstrap-server $B describekafka-2:19092 (id: 2 rack: null isFenced: false) -> ( kafka-1:19092 (id: 1 rack: null isFenced: false) -> ( kafka-3:19092 (id: 3 rack: null isFenced: false) -> ( Feature: eligible.leader.replicas.version SupportedMinVersion: 0 SupportedMaxVersion: 1 FinalizedVersionLevel: 1 Feature: metadata.version SupportedMinVersion: 3.3-IV3 SupportedMaxVersion: 4.3-IV0 …
Common mistakes and misconceptions
| People think | What is true |
|---|---|
| Kafka needs ZooKeeper. | Not since Kafka 4.0. Guides that start ZooKeeper are for old versions. |
| The controller handles client traffic. | Producers and consumers talk only to brokers. The controller handles metadata. |
| Two controllers are safer than one. | Two need both to form a majority, so they survive no failure and have two things that can fail. Use three. |
| If the controllers are down, Kafka is down. | Existing leaders keep working. What stops is every change: elections, new topics, settings. |
| Partition data also uses majority voting. | Only metadata does. Partition data uses the in-sync set from module 04. |
| Combined mode is fine for production because it saves servers. | It ties the quorum's health to broker load. Use dedicated controllers. |
| Upgrading the software upgrades the cluster. | New formats start only when you raise metadata.version. Until then you can still roll back the software. |
Staff-level questions
Why did Kafka replace ZooKeeper?
Three reasons. Operations: one system to deploy, secure, monitor and upgrade in place of two. Scale: with ZooKeeper, a new controller had to load all metadata before it could act, which took minutes on large clusters; a KRaft controller already has the log in memory and takes over in about a second. Correctness: the old design had two sources of truth, ZooKeeper and the controller's memory, that could disagree; a single ordered log removes that class of bug.
Where do you place three controllers in a cloud region with three zones? With two zones?
Three zones: one controller in each. Any one zone can fail and a majority remains.
Two zones: there is no good answer. One zone must hold two controllers, and losing that zone loses the majority. Either add a third location only for a controller, which is small and cheap, or accept that one specific zone failure stops metadata changes and write the manual recovery steps in advance.
How do you upgrade a KRaft cluster safely?
Read the upgrade notes for the target version first. Upgrade the software one server at a time: controllers first, the active controller last among them, then brokers, each time waiting for the server to catch up and for under-replicated partitions to return to zero. Run on the new software with the old metadata.version for a while. Only then raise the version with kafka-features.sh upgrade.
The first part can be rolled back by installing the old software. Lowering the metadata version afterwards is limited, so treat that step as the point of no return. unverified: the order and downgrade limits are from memory
What limits the number of partitions in one cluster?
Each partition is metadata the controller must hold in memory, write records for, and send to every broker. Each replica is open files and memory on a broker. When a broker fails, the controller writes a change for every partition that broker touched, and the brokers apply them.
KRaft raised the practical limit a great deal compared with ZooKeeper, because failover no longer reloads everything. But failover time and broker start time still grow with the count. Test with your own numbers before you rely on a published limit.
What do you monitor on the controllers?
That exactly one active controller exists. The quorum's leader epoch: a rising number means elections. The lag of each voter behind the leader. The rate of metadata records and the time to apply them. The count of fenced brokers. And disk space and latency on the metadata log directory, because a slow disk there slows every decision.
A broker is alive but cut off from the controllers. Can it cause damage?
It keeps believing it leads its partitions. The controller fences it after 9 seconds and elects new leaders, with a higher leader epoch for each partition.
Producers with acks=all are protected: the cut-off broker cannot get its followers to confirm, so it cannot acknowledge. When it returns, it sees the higher epoch and truncates what it wrote alone. Producers with acks=1 that can still reach it will get "success" for records that are later cut off. This is one more reason to use acks=all.
Self-test
1. What does the active controller do?
It makes every metadata decision, such as leader elections and topic creation, and writes each one to the metadata log.
2. How many of five controllers can fail before metadata changes stop?
Two. Three of five is a majority.
3. What is the name of the metadata topic, and how many partitions does it have?
__cluster_metadata, with one partition.
4. When is a metadata record committed?
When a majority of the controllers have it.
5. What is a fenced broker?
A registered broker that the controller does not allow to lead partitions or count in an in-sync set.
6. After how long without a heartbeat is a broker fenced, by default?
9 seconds: broker.session.timeout.ms.
7. Why does a controller vote only for a candidate whose log is at least as complete as its own?
So that the new leader has every committed record. A majority has each committed record, and a majority must vote for the winner, so at least one voter would refuse a candidate that lacks one.
8. What is a metadata snapshot for?
It holds the full state at one offset, so that a server can start from it and old log records can be deleted.
9. Which command shows the current active controller?
kafka-metadata-quorum.sh describe --status; read LeaderId.
10. What is combined mode and where should you use it?
One process that is both broker and controller. Use it for development and tests, not for production.