Kafka without ZooKeeper: KRaft in production
Table of contents
- Key takeaways
- Why ZooKeeper was a problem
- What KRaft improves
- What changes in operations
- How to migrate from ZooKeeper to KRaft step by step
- Step 0: run the brokers on 3.9 with ZooKeeper
- Step 1: create the KRaft controller with the cluster ID
- Step 2: restart the brokers with migration enabled
- Step 3: switch the brokers to KRaft mode
- Step 4: finalize and shut down ZooKeeper
- Step 5: upgrade to Kafka 4.x
- Where it can hurt
- When to wait before migrating
- My take
- Frequently asked questions
- Can I upgrade straight to Kafka 4.x if my cluster still uses ZooKeeper?
- Does the migration to KRaft require downtime?
- What can break when removing ZooKeeper?
- Sources
Tested with apache/kafka 3.9.2 and 4.4.0 · Docker on arm64 · verified
Updated: 2026-10-27
To remove ZooKeeper from Kafka you have to go through 3.9. I migrated a three-broker Kafka 3.9.2 ZooKeeper cluster to KRaft in Docker and then upgraded it to Kafka 4.4.0: five phases with rolling restarts, the config and commands for each one, and all 100,000 messages intact at the end.
To remove ZooKeeper from a Kafka cluster you have to go through 3.9: it is the last version that talks to ZooKeeper and the only one from which the official guide migrates the metadata to KRaft without stopping the service. After that you can move to 4.x. I ran the whole migration in Docker on 27 October 2026: a three-broker ZooKeeper cluster on Kafka 3.9.2, one KRaft controller, four rolling restarts and the final upgrade to Kafka 4.4.0. The 100,000 messages in the test topic and the consumer group’s committed offsets came through intact.
Kafka 4.0 arrived on March 18, 2025, according to the official Apache Software Foundation announcement[1], and ZooKeeper mode went with it. The plan was years old: KIP-500 was filed in October 2019[2] and KRaft mode was marked production-ready in Kafka 3.3, at the end of 2022. The ZooKeeper to KRaft migration (KIP-866) shipped as early access in 3.4. Since 4.0 a broker can no longer start in ZooKeeper mode.
For the data infrastructure context where Kafka operates, the analysis of Delta Lake and Iceberg comparison describes the storage layer that complements Kafka in modern architectures. The incremental migration pattern with external dependencies also applies to the context of Kubernetes 1.33 improvements, where platform upgrades follow similar logic.
Key takeaways
-
KRaft stores cluster metadata in an internal topic
__cluster_metadatausing Raft for consensus; no external system. -
Kafka 4.x does not start in ZooKeeper mode. The migration happens before, on 3.9.x, and the 3.9 guide supports upgrading to it from any 0.8.x through 3.8.x.
-
The order is fixed: KRaft controller in migration mode, brokers with migration enabled, brokers in KRaft mode and, last, the controller without ZooKeeper. Until that last step you can roll back; after it, you cannot.
-
In the test, the controller copied the metadata in 145 ms (62 records) as soon as the last broker came back with migration enabled.
-
ZooKeeper metrics disappear; new controller quorum metrics appear that legacy dashboards need to pick up.
Why ZooKeeper was a problem
For more than a decade Kafka depended on ZooKeeper to hold cluster metadata: who is the controller, which brokers are alive, which topics exist, which partitions go to which replica. ZooKeeper is solid but was not designed for the metadata volume a large Kafka cluster generates. There was a practical ceiling: clusters with hundreds of thousands of partitions hit ZooKeeper as the bottleneck.
The second problem was operational: running two different distributed systems with two different consensus models. A team had to know Kafka and also ZooKeeper, with its configuration, commands and failure modes. Every cluster expansion meant thinking about both sides. Every failure forced you to decide whether it was a Kafka failure or a ZooKeeper one.
KRaft is the answer: Kafka itself maintains its metadata using Raft as the consensus algorithm, inside a quorum of nodes with the controller role. There is no external system. Metadata is stored in an internal topic called __cluster_metadata and replicated following the same rules as any other topic.
What KRaft improves
The first improvement is the partition ceiling. According to Confluent’s KRaft documentation[3], the design allows clusters with the potential to scale to millions of partitions. ZooKeeper started suffering in practice in the hundreds of thousands. Knowing the ceiling is high changes how you design topics: you can be more generous with partitions without fearing that metadata becomes the bottleneck.
The second is controller failover. With ZooKeeper, a new controller had to load the whole state from ZooKeeper before acting. In KRaft, standby controllers already hold the replicated metadata log and take over without that load.
On a three-broker cluster you will not notice: in the test, each broker took between 3 and 6 s to come back after a restart, with ZooKeeper and without it. The difference shows up with tens of thousands of partitions.
The third improvement is operational. One technology, one configuration, one set of commands. Admin commands like kafka-topics and kafka-configs work the same way, but there is no longer an additional system behind them that can fail on its own.
What changes in operations
The deployment model changes in two places. First, brokers can take the controller role in addition to the broker role. In a small cluster, three nodes doing both is common. In a large cluster they are separated: three or five dedicated controller nodes, and the rest as pure brokers.
Second, the cluster ID concept appears from the start. In KRaft you format the storage with kafka-storage before the first start. A migration does not generate a new ID: it reuses the one ZooKeeper already holds, as step 1 of the migration shows.
Monitoring changes too. ZooKeeper metrics disappear. New metrics appear for the controller quorum: how many replicas are lagging, how long metadata updates take, how many events per second the active controller processes. Legacy Grafana dashboards need adjusting.
How to migrate from ZooKeeper to KRaft step by step
The migration has five phases, and every one of them is a rolling restart, without stopping the cluster. I followed them as the official Kafka 3.9 migration guide[4] describes them, with the official apache/kafka:3.9.2 and apache/kafka:4.4.0 images in Docker on arm64.
The lab had one ZooKeeper, three brokers (broker.id 1, 2 and 3) and a new KRaft controller. Before starting I created the pedidos topic with 6 partitions and replication factor 3 and sent 100,000 messages of 200 bytes with acks=all. Then I consumed 40,000 with the lectores group, to check at the end that the data and the committed offsets survive. In production, run three controllers, not one.
Step 0: run the brokers on 3.9 with ZooKeeper
Every broker must run Kafka 3.9.x with the inter-broker protocol pinned to 3.9. The guide does not allow changing the metadata version during the migration, so this line stays in each broker’s server.properties until step 3:
broker.id=1
listeners=PLAINTEXT://:9092
zookeeper.connect=zk:2181
inter.broker.protocol.version=3.9
Step 1: create the KRaft controller with the cluster ID
The controller is formatted with the same cluster ID that ZooKeeper stores. You read it with zookeeper-shell.sh, and the controller’s node.id must not match any existing broker.id, because brokers and controllers share the ID space:
bin/zookeeper-shell.sh zk:2181 get /cluster/id
bin/kafka-storage.sh format --standalone \
--cluster-id=g3x3R7lsSYqcRtSON2SpPg -c controller.properties
bin/kafka-server-start.sh controller.properties
The first command returns a JSON object with an id field, which is the value you pass to --cluster-id. The controller.properties file enables the migration and points at ZooKeeper:
process.roles=controller
node.id=3000
controller.quorum.bootstrap.servers=ctl:9093
controller.listener.names=CONTROLLER
listeners=CONTROLLER://:9093
zookeeper.metadata.migration.enable=true
zookeeper.connect=zk:2181
inter.broker.listener.name=PLAINTEXT
Step 2: restart the brokers with migration enabled
Each broker stays in ZooKeeper mode but now knows the KRaft quorum. Add these four lines to its configuration and restart them one at a time:
zookeeper.metadata.migration.enable=true
controller.quorum.bootstrap.servers=ctl:9093
controller.listener.names=CONTROLLER
listener.security.protocol.map=PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT
The metadata copy starts by itself when the last broker comes back. In the test, the controller log confirmed it as soon as the third broker returned: “Completed migration of metadata from ZooKeeper to KRaft. 62 records were generated in 145 ms”. That was 2 topics, 56 partitions, 3 configs and one producer-ID record. From then on the KRaft controller is the active one and also writes to ZooKeeper, which is what makes a rollback possible.
Step 3: switch the brokers to KRaft mode
Now each broker leaves ZooKeeper. Replace broker.id with node.id using the same number, add process.roles=broker and delete zookeeper.connect, zookeeper.metadata.migration.enable and inter.broker.protocol.version:
node.id=1
process.roles=broker
listeners=PLAINTEXT://:9092
controller.quorum.bootstrap.servers=ctl:9093
controller.listener.names=CONTROLLER
listener.security.protocol.map=PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT
Another rolling restart. If you use kafka.security.authorizer.AclAuthorizer, switch it in this step to org.apache.kafka.metadata.authorizer.StandardAuthorizer. Each broker came back in 6 s.
Step 4: finalize and shut down ZooKeeper
Remove zookeeper.metadata.migration.enable and zookeeper.connect from the controller and restart it. This is the point of no return. Up to here the guide documents how to revert to ZooKeeper depending on the last completed step; after it, you cannot. With the controller off ZooKeeper, I shut ZooKeeper down and checked the state:
bin/kafka-topics.sh --bootstrap-server b1:9092 --describe --topic pedidos
bin/kafka-get-offsets.sh --bootstrap-server b1:9092 --topic pedidos
bin/kafka-consumer-groups.sh --bootstrap-server b1:9092 \
--describe --group lectores
The 6 partitions still had their three in-sync replicas, the end offsets added up to 100,000 and the lectores group kept its 40,000 committed messages.
Step 5: upgrade to Kafka 4.x
With the cluster on KRaft, the move to 4.x is one more rolling restart: first the controller, then each broker, with the new image and the same configuration. The 4.4 guide supports upgrading from any 3.3.x onwards. When it is done, finalize the cluster features at the new version:
bin/kafka-features.sh --bootstrap-server b1:9092 \
upgrade --release-version 4.4
bin/kafka-features.sh --bootstrap-server b1:9092 describe
The command raised metadata.version from 3.9-IV0 to 4.4-IV2 and enabled, among others, kraft.version 1 and group.version 1. Afterwards, the lectores group read exactly the 60,000 messages it had left. From creating the controller to that final read took 2 min 14 s in the lab. On a real cluster, the time is set by how long each broker takes to recover its replicas after each restart.
Where it can hurt
There are places where the migration hurts:
-
External tools that talk directly to ZooKeeper: old management tools, internal scripts that read ZooKeeper nodes for inventory, third-party integrations that assume the old model. All of that stops working and has to be catalogued before migrating so it can be rewritten against the AdminClient.
-
Features KRaft does not cover yet: the 3.9 guide warns that using a feature not fully implemented in KRaft blocks the migration, and that a JBOD broker with a failed directory cannot migrate until it is repaired.
-
ACL and quota configuration: with ZooKeeper it was common to set them directly on the nodes. With KRaft they move to
kafka-configsand theStandardAuthorizer. The migration copies what is there, but any deployment automation that touches ZooKeeper nodes has to be rewritten.
When to wait before migrating
Some teams should wait:
-
Cluster on Kafka 2.x: the first step is upgrading to 3.9.x with ZooKeeper, stabilizing, and only then planning KRaft. They are two separate changes and should not share a change window.
-
Integrations with proprietary software that speaks ZooKeeper: before migrating, verify explicitly with each critical vendor.
-
In the middle of another big migration: if the team is moving to Kubernetes or changing cloud provider at the same time, adding KRaft to the same project multiplies the risk. Better to sequence.
My take
KRaft is a clear improvement, and the jump to Kafka 4.x is a sensible migration for any cluster that plans it with time. The official procedure works as written: the lab needed no step outside the guide. But this is not a migration for a Friday afternoon: it needs an inventory of external dependencies, a rehearsal in a test environment and a thought-through change window.
The practical advice is to rehearse the five phases on a copy of the cluster and measure how long each broker takes to recover its replicas. Then catalogue every external integration that touches ZooKeeper and prepare the deployment scripts in advance. The rehearsal tells you how long the real window will be and whether a feature you use blocks the migration.
What Kafka 4.0 consolidates is a simple idea: one technology, one consensus model, one thing to learn. For anyone operating Kafka daily, this is the most significant change in years. For anyone who just produces and consumes messages, it is invisible. Both are good signs.
This article is also available in Spanish: Kafka sin ZooKeeper: KRaft en producción.
Frequently asked questions
Can I upgrade straight to Kafka 4.x if my cluster still uses ZooKeeper?
No: Kafka 4.x does not start in ZooKeeper mode, so the migration to KRaft has to be finished first. The path is to upgrade to 3.9.x and create the KRaft controller with the cluster ID that ZooKeeper stores. Then you restart the brokers with migration enabled, switch them to KRaft mode and remove ZooKeeper from the controller. Once the cluster runs on KRaft, the 4.4 guide supports upgrading from any 3.3.x onwards.
Does the migration to KRaft require downtime?
No: every phase is a rolling restart of one node at a time. In the Docker test, with three brokers and replication factor 3, the 100,000 messages and the consumer group’s committed offsets survived. They were intact after the migration and after the upgrade to 4.4.0.
The metadata copy took 145 ms. What stretches the window on a real cluster is how long each broker takes to recover its replicas after each restart.
What can break when removing ZooKeeper?
Anything that talks directly to ZooKeeper: old management tools, internal scripts that read its nodes for inventory, and third-party integrations that assume the old model. Catalogue it first and rewrite it against the AdminClient. ACLs move to the StandardAuthorizer, and legacy Grafana dashboards need the new controller quorum metrics. Remember that, after finalizing, going back to ZooKeeper is no longer possible.
Sources:
- Apache Kafka: 4.0.0 release announcement[1]
- Apache Kafka 3.9: ZooKeeper to KRaft migration[4]
- Apache Kafka: upgrading to 3.9[5]
- Apache Kafka: upgrading to 4.4[6]
- Apache Software Foundation Jira: KIP-500[2]
- KIP-866: ZooKeeper to KRaft migration[7]
- Confluent Developer: KRaft[3]