Overview
You migrate on 3.9, not on 4.0, because the migration needs a release that still speaks ZooKeeper. The cluster moves through named phases. It starts in the initial phase, with every broker in ZooKeeper mode. A new KRaft controller quorum then loads the metadata from ZooKeeper. While some brokers are still in ZooKeeper mode and a KRaft controller exists, you are in the hybrid phase. When every broker runs in KRaft mode but the controller still writes metadata to ZooKeeper as well, you are in the dual-write phase. Finalizing stops those writes. Until you finalize you can roll back; after, you cannot, which is why the documentation notes that some people wait a week or two first. The 3.9 documentation also lists traps: do not change the metadata version during the migration; KRaft node ids share a namespace with broker ids, so they must differ; the migration can stall if the ZooKeeper Security Migration Tool was run before (delete the /migration node); a static controller quorum cannot be converted to a dynamic one; and JBOD with several log directories is listed as a feature KRaft does not fully implement, so a cluster that depends on it cannot migrate yet.
Analogy
Key configuration
# 1. Prepare: brokers on 3.9.x with inter.broker.protocol.version=3.9
# 2. Provision KRaft controllers, using the ZooKeeper cluster's id
process.roles=controller
node.id=3000 # must differ from every broker.id
controller.quorum.bootstrap.servers=localhost:9093
controller.listener.names=CONTROLLER
listeners=CONTROLLER://:9093
zookeeper.metadata.migration.enable=true
zookeeper.connect=localhost:2181
inter.broker.listener.name=PLAINTEXT
bin/kafka-storage.sh format --standalone --cluster-id=<zk-cluster-id> -c config/kraft/controller.properties
# 3. Rolling restart of every broker in migration mode
zookeeper.metadata.migration.enable=true
controller.quorum.bootstrap.servers=localhost:9093
controller.listener.names=CONTROLLER # also add it to listener.security.protocol.map
# Migration starts when the last ZooKeeper broker restarts. The active controller logs:
# Completed migration of metadata from Zookeeper to KRaft
# 4. Roll each broker into KRaft, one at a time
process.roles=broker
node.id=<its old broker.id>
# remove zookeeper.connect and zookeeper.metadata.migration.enable; do not set the IBP
# authorizer.class.name: AclAuthorizer -> org.apache.kafka.metadata.authorizer.StandardAuthorizer
# All brokers in KRaft mode: dual-write phase. You can still roll back.
# 5. Finalize: remove zookeeper.metadata.migration.enable from each controller,
# restarting them one at a time. ZooKeeper can then be switched off.Production tip
Check yourself
A migrating cluster has every broker in KRaft mode, but the controller still writes metadata to ZooKeeper. What phase is this, and can you still roll back?
Sources
- KRaft operations and ZooKeeper to KRaft migration (3.9 documentation) (opens in a new tab)
- Upgrading to 4.0 (Apache Kafka documentation) (opens in a new tab)
- Kafka 3.9.0 release announcement (opens in a new tab)
Checked against Kafka 4.3 documentation on Oct 2026. Draft: not yet reviewed by a Kafka specialist. Releases move on, so confirm against the current documentation before you act on a number.