Kafka / 05 KRaft
All modules
Module 05 · Day 4 · Kafka 4.3

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.

Controllers
offset 0

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.

offset 1

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.

ControllersMajorityFailures survived
110
321
532

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 replayValue
offset 2

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 typeCountMeaning
NO_OP_RECORD4,731Written every 500 ms when nothing else happens, so that every server can tell the log is alive.
PARTITION_CHANGE_RECORD1,533A partition's leader or in-sync set changed. Each broker stop in the labs caused many.
PARTITION_RECORD79A partition was created, with its replicas, in-sync set and leader.
BROKER_REGISTRATION_CHANGE_RECORD51A broker was fenced or unfenced.
CONFIG_RECORD23A topic or broker setting changed.
TOPIC_RECORD19A topic was created.
REGISTER_BROKER_RECORD13A broker started and registered.
FEATURE_LEVEL_RECORD6The cluster's feature versions were set.

Electing the active controller

  1. Every controller follows the current leader and fetches from it. Time is divided into epochs; each epoch has at most one leader.
  2. 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.
  3. If a majority would, it starts a new epoch and asks for real votes.
  4. 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.
  5. 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

1 StartReads meta.properties.
2 RegisterGets a broker epoch. Starts fenced.
3 Catch upReplays the metadata log.
4 UnfencedCan lead partitions.
5 HeartbeatEvery 2 seconds.
6 StopClean, or fenced after 9 seconds.

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.

offset 3

Every relevant config

All defaults below were read from the lab cluster with kafka-configs.sh --describe --all.

ConfigDefaultWhat it doesWhen to change it
process.rolesnone; the lab sets broker,controllerWhat this server is: broker, controller, or both.Use separate controllers in production.
node.idnone (required)The unique number of this server.Set once. Never reuse a number for a different server.
controller.quorum.votersnone; the lab sets threeA 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.serversemptyAddresses 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.namesnone; the lab sets CONTROLLERWhich network listener the controllers use.Keep the controller listener on a private network.
controller.quorum.election.timeout.ms1000How long a candidate waits for votes before it tries again.Raise on slow or far-apart networks.
controller.quorum.fetch.timeout.ms2000Time without contact with the leader before a controller starts an election.Raise if elections happen with no real failure.
controller.quorum.election.backoff.max.ms1000Longest random wait before a new election attempt, to stop two candidates from tying again and again.Rarely.
controller.quorum.request.timeout.ms2000Timeout for one request between controllers.Rarely.
broker.heartbeat.interval.ms2000How often a broker reports to the active controller.Rarely.
broker.session.timeout.ms9000Time 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.dirnull (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.snapshots20971520 (20 MiB)Write a snapshot after this much new metadata.Rarely.
metadata.log.max.snapshot.interval.ms3600000 (1 hour)Write a snapshot at least this often when there are new records.Rarely.
metadata.max.retention.bytes, metadata.max.retention.ms104857600 (100 MiB), 604800000 (7 days)How much of the metadata log and its snapshots to keep.Rarely.
metadata.log.segment.bytes1073741824 (1 GiB)Segment size of the metadata log.Rarely.
metadata.max.idle.interval.ms500The active controller writes a no-op record this often when idle.Rarely.
controller.quorum.auto.join.enablefalseLets a new controller add itself to the quorum.Only with the newer, changeable quorum. unverified
offset 4

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 LeaderId in kafka-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 --describe and --create both 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: true for that broker in kafka-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".
offset 5

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:19092

Step 1. Look at the quorum

k kafka-metadata-quorum.sh --bootstrap-server $B describe --status
k kafka-metadata-quorum.sh --bootstrap-server $B describe --replication
ClusterId:              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-state
cluster.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 2
LeaderId: 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 --status
LeaderId:        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 describe
kafka-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  …
offset 6

Common mistakes and misconceptions

People thinkWhat 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.
offset 7

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.

offset 8

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.