Apache Kafka 4.0 runs only in KRaft mode. If your brokers still have zookeeper.connect in server.properties, there is no 4.x release you can roll onto them. The upgrade notes say it plainly: clusters in ZooKeeper mode "have to be migrated to KRaft mode" before they can upgrade. For teams running change data capture (CDC), that is two projects. First comes a metadata migration on the Kafka 3.9 bridge release. Then comes a 4.x upgrade that needs Java 17 on Kafka Connect workers, drops client protocol versions older than Kafka 2.1, and removes MirrorMaker 1. This guide covers both for a cluster that carries Debezium or other Kafka Connect pipelines. It explains how to size the controller quorum, how the migration phases run, where each rollback point is, and what to check on the Connect side.
- Kafka 4.0.0 (18 March 2025) removed ZooKeeper mode. As of November 2026, the newest release is 4.4.0 (22 October 2026), and 4.3.2 and 4.2.2 are also listed as supported.
- ZooKeeper clusters migrate on the 3.9 bridge release. The 3.9 docs require brokers on 3.9.1 or later with
inter.broker.protocol.version=3.9; 3.9.2 is the newest 3.x patch. - The migration runs in four moves: provision controllers, put brokers in migration mode, restart them as KRaft brokers, then finalize. You can revert to ZooKeeper until you finalize, and not after.
- Then roll to 4.x on Java 17 and finalize with
kafka-features.sh upgrade --release-version. Metadata cannot be downgraded after that step. - Connect and Debezium never talk to ZooKeeper, and their offsets live in Kafka topics. The CDC work is Java 17 workers, removed SMT options and APIs, MirrorMaker 1, and dashboards.
Scope and review date: November 2026. This post covers self-managed Apache Kafka clusters in ZooKeeper mode moving to Kafka 4.x through the 3.9 bridge release, with Kafka Connect and Debezium 3.x pipelines attached. Managed services and Kubernetes operators have their own migration procedures, so follow your provider's documentation there. Configs, commands, and version numbers were checked against the Apache Kafka 3.9 and 4.4 documentation and the Debezium release notes on 9 November 2026. They were not run in a lab, so rehearse them on a staging cluster.
What did Kafka 4.0 remove, and which 4.x release is current?
The Kafka upgrade notes open the 4.0 changes with this: "Apache Kafka 4.0 only supports KRaft mode - ZooKeeper mode has been removed." Metadata that lived in ZooKeeper znodes now lives in the __cluster_metadata log, replicated by a Raft quorum of controllers.
The Kafka downloads page lists 4.4.0 (22 October 2026), 4.3.2 (23 October 2026), and 4.2.2 (29 September 2026) as the supported releases. Every 3.x release, including 3.9.2 (21 February 2026), is archived. Treat 3.9 as a place to migrate, not a place to stay.
# On each broker: which mode is this cluster in?
grep -E '^(process\.roles|zookeeper\.connect|broker\.id|node\.id|inter\.broker\.protocol\.version)=' \
/etc/kafka/server.propertiesInterpretation: a zookeeper.connect line with no process.roles means ZooKeeper mode, and you need the full migration below. A process.roles=broker line means the cluster is already KRaft, and you can skip to the 4.x upgrade section. KRaft clusters older than 3.3 should go to 3.9.x first, because 4.x accepts upgrades only from 3.3.x onwards.
ZooKeeper mode and KRaft mode side by side
Switch modes to see what changes in configs, tooling, and operations
ZooKeeper mode
| Area | ZooKeeper mode |
|---|---|
| Where metadata lives | ZooKeeper ensemble (znodes) |
| Controller | One broker, elected through the /controller znode |
| Node identity | broker.id, optionally auto-generated |
| Version gate | inter.broker.protocol.version |
| How nodes find metadata | zookeeper.connect |
| ACL authorizer | kafka.security.authorizer.AclAuthorizer |
Change advertised.listeners | Dynamic config |
| Supported releases | 3.9.x and earlier |
KRaft mode
| Area | KRaft mode |
|---|---|
| Where metadata lives | __cluster_metadata log on the controller quorum |
| Controller | 3 or 5 dedicated controllers; the Raft leader is active |
| Node identity | node.id; broker.id is deprecated in 4.4 and removed in 5.0 |
| Version gate | metadata.version, set with kafka-features.sh |
| How nodes find metadata | controller.quorum.bootstrap.servers (dynamic) or controller.quorum.voters (static) |
| ACL authorizer | org.apache.kafka.metadata.authorizer.StandardAuthorizer |
Change advertised.listeners | Restart the node |
| Supported releases | 3.3.x onwards; the only mode from 4.0 |
Tooling breaks first: any script, exporter, or admin UI that reads ZooKeeper directly stops working once ZooKeeper is gone. Inventory them before the migration, not after finalize.
What is the upgrade path from a ZooKeeper cluster to Kafka 4.x?
Three stages, each with its own rolling restarts. The 4.4 KRaft operations page says: "The last bridge release is Kafka 3.9." The 3.9 KRaft documentation, updated in October 2026, says brokers "must be upgraded to software version 3.9.1" and run with inter.broker.protocol.version set to 3.9 before the migration starts.
- Bridge. Upgrade the ZooKeeper-mode cluster to 3.9.x and raise the inter-broker protocol to 3.9. Use 3.9.2: it adds the KIP-1252 compatibility flag for an
AlterConfigPolicyinconsistency between the two modes. - Migrate. Move metadata from ZooKeeper to a KRaft controller quorum, still on 3.9.
- Upgrade. Roll the KRaft cluster from 3.9 to 4.x and finalize the new metadata version. The 4.4 notes accept servers "from any version 3.3.x through 4.3.0", so 3.9 can go straight to 4.4.
# Record the cluster ID and the IBP before you start
bin/zookeeper-shell.sh zk-1:2181 get /cluster/id
grep '^inter.broker.protocol.version=' /etc/kafka/server.propertiesInterpretation: the cluster ID returned here must be reused when you format the controllers. If the IBP is not 3.9 on every broker, finish that rolling restart first. The 3.9 docs warn that changing the metadata version during the migration "may break the cluster".
The 3.9 Java page lists Java 8, 11, 17, and 21 as fully supported, while 4.x brokers, Connect, and tools require Java 17. Moving brokers and Connect workers to Java 17 while still on 3.9 keeps the JDK change out of the 4.x upgrade window.
How should you size and place the KRaft controller quorum?
A KRaft controller quorum needs a majority alive to accept metadata changes. The 3.9 and 4.4 docs give the same rules. Run 3 or 5 controllers: 3 tolerate one failure and 5 tolerate two, because tolerating N failures takes 2N + 1 controllers. Combined broker and controller nodes are "not recommended in critical deployment environments". For a typical cluster, the docs estimate that 5 GB of memory and 5 GB of disk for the metadata log directory is sufficient.
Place each controller in a different failure domain, such as one per availability zone, so the quorum survives a zone outage. Controller node.id values share one namespace with broker IDs, so pick a range no broker uses (the docs use 3000 and up).
# controller.properties for a 3-controller static quorum in migration mode (Kafka 3.9)
process.roles=controller
node.id=3000
controller.quorum.voters=3000@ctl-0:9093,3001@ctl-1:9093,3002@ctl-2:9093
controller.listener.names=CONTROLLER
listeners=CONTROLLER://:9093
# Enable the migration
zookeeper.metadata.migration.enable=true
zookeeper.connect=zk-1:2181,zk-2:2181,zk-3:2181
# Lets the KRaft controller send RPCs to ZooKeeper-mode brokers
inter.broker.listener.name=PLAINTEXTInterpretation: the 3.9 sample uses a single standalone controller with controller.quorum.bootstrap.servers, a dynamic quorum. A static quorum lists every voter in controller.quorum.voters. Static cannot become dynamic on 3.9, but the 4.4 docs show the switch from release version 4.1 by upgrading kraft.version to 1.
Where the controller quorum sits in a CDC pipeline
Debezium and Connect talk only to brokers; ZooKeeper drops out after finalize
Metadata, not data: the migration moves cluster metadata into the quorum. Topic data, including Connect's internal topics and Debezium's schema history topic, stays on the brokers.
How do you run the ZooKeeper to KRaft migration on Kafka 3.9?
The 3.9 docs name the phases: initial (all brokers in ZooKeeper mode), initial metadata load, hybrid (some brokers still in ZooKeeper mode under a KRaft controller), dual-write (all brokers in KRaft mode, the controller still writes to ZooKeeper), and finalized. Each move below is a rolling restart, so partitions stay available as long as replication factors and min.insync.replicas allow one broker down.
ZooKeeper to KRaft migration, then Kafka 4.x
Nine steps; steps 7 and 9 are one-way, and step 7 is the last ZooKeeper rollback point
-
Bridge to 3.9.x with IBP 3.9
Rolling upgrade of the ZooKeeper-mode brokers to 3.9.1 or later, then set the inter-broker protocol to 3.9.
inter.broker.protocol.version=3.9 -
Format and start the controllers
Reuse the existing cluster ID. Controllers start with migration enabled and a ZooKeeper connection.
bin/kafka-storage.sh format \ --cluster-id <zk-cluster-id> \ -c config/kraft/controller.properties -
Put brokers in migration mode
Add the migration flag and controller settings, then roll the brokers. The metadata copy starts after the last broker restarts.
zookeeper.metadata.migration.enable=true -
Confirm the metadata load
The active controller logs one INFO line when the copy finishes. The cluster is now in the hybrid phase.
grep 'Completed migration of metadata' \ /var/log/kafka/server.log -
Restart brokers as KRaft brokers
Replace broker.id with node.id (same value), add process.roles=broker, remove ZooKeeper settings. Dual-write starts when the last one is done.
process.roles=broker node.id=0 -
Burn in under dual-write
The docs note that some users wait a week or two here. Run month-end CDC loads and connector restarts in this window.
-
Finalize the migration
No return to ZooKeeperRemove the migration flag and ZooKeeper settings from each controller and restart them one at a time.
# zookeeper.metadata.migration.enable=true # zookeeper.connect=zk-1:2181 -
Roll every node to Kafka 4.x
Java 17 or later on every node. One node at a time: stop, swap the binaries, start, check under-replicated partitions.
-
Finalize the 4.x metadata version
No metadata downgradeOnly after the 4.x cluster has run cleanly. The 4.0 notes say metadata downgrade is not supported after this.
bin/kafka-features.sh \ --bootstrap-server broker-0:9092 \ upgrade --release-version 4.4
Rollback point: until step 7, the 3.9 docs give a revert path to ZooKeeper for every completed step. After step 7, the docs offer no way back to ZooKeeper, and after step 9 a 4.x cluster cannot lower its metadata version.
Broker settings for migration mode
Adapted from the 3.9 sample. broker.id must be an explicit non-negative integer even when broker.id.generation.enable is on.
# server.properties, ZooKeeper-mode broker entering migration mode (Kafka 3.9)
broker.id=0
listeners=PLAINTEXT://:9092
advertised.listeners=PLAINTEXT://broker-0:9092
listener.security.protocol.map=PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT
inter.broker.protocol.version=3.9
zookeeper.metadata.migration.enable=true
zookeeper.connect=zk-1:2181,zk-2:2181,zk-3:2181
controller.quorum.voters=3000@ctl-0:9093,3001@ctl-1:9093,3002@ctl-2:9093
controller.listener.names=CONTROLLERInterpretation: use the same quorum setting as the controllers, and add the controller listener to listener.security.protocol.map. With TLS or SASL, that entry must match the controllers' listener.
Broker settings for step 5
# server.properties, same broker restarted in KRaft mode
process.roles=broker
node.id=0
listeners=PLAINTEXT://:9092
advertised.listeners=PLAINTEXT://broker-0:9092
listener.security.protocol.map=PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT
controller.quorum.voters=3000@ctl-0:9093,3001@ctl-1:9093,3002@ctl-2:9093
controller.listener.names=CONTROLLER
# Removed: inter.broker.protocol.version, zookeeper.metadata.migration.enable,
# zookeeper.connect, control.plane.listener.name
# If you used AclAuthorizer:
authorizer.class.name=org.apache.kafka.metadata.authorizer.StandardAuthorizerInterpretation: node.id must equal the old broker.id; the docs say the broker must keep the same ID. KRaft uses the metadata.version feature flag instead of an IBP.
The migration can stall if the ZooKeeper Security Migration Tool was ever run (KAFKA-19480); the workaround is to delete the malformed /migration znode. A broker with multiple log directories shuts down on any directory failure during the migration, and can migrate only after the directories are repaired (KAFKA-16431).
Where are the rollback points, and how do you revert?
The 3.9 docs include a revert table keyed on the last step you fully completed. Before brokers enter migration mode, you only deprovision the controllers. After that, reverting needs ZooKeeper edits and one or two broker rolls. Once the migration is finalized, the docs say "you cannot revert".
| Last completed step | How to revert |
|---|---|
| Bridge to 3.9 (step 1) | Nothing to undo for the migration; the cluster is still in ZooKeeper mode |
| Controllers provisioned (step 2) | Deprovision the KRaft controller quorum |
| Brokers in migration mode (steps 3 and 4) | Deprovision the quorum, fix the controller epoch, delete /controller and /migration, remove the migration configs, roll brokers once |
| Brokers in KRaft mode (step 5) | Restore ZooKeeper configs and roll brokers, deprovision the quorum, run the same ZooKeeper steps, then roll brokers a second time |
| Finalized (step 7) | Not possible |
# Revert from step 3 or later, right after deprovisioning the controllers
bin/zookeeper-shell.sh zk-1:2181 get /controller_epoch
bin/zookeeper-shell.sh zk-1:2181 get /migration # note kraft_controller_epoch
# Only if the KRaft epoch is higher: pick a value above it
bin/zookeeper-shell.sh zk-1:2181 set /controller_epoch 42
bin/zookeeper-shell.sh zk-1:2181 delete /controller
bin/zookeeper-shell.sh zk-1:2181 delete /migrationInterpretation: the epoch check is new in the October 2026 revision of the docs (KAFKA-20488). Partition state znodes carry the KRaft controller epoch during the migration. A ZooKeeper controller elected with a lower epoch fails partition state changes, because it assumes another controller with a higher epoch exists. The value 42 is a placeholder. Run these commands quickly: the cluster has no controller until /controller is deleted.
When reverting from step 5, the docs say zookeeper.metadata.migration.enable must stay true during the first broker roll and come out only on the second. Write both rolls into the runbook before you start.
What breaks when you move from KRaft 3.9 to Kafka 4.x?
On a finalized KRaft 3.9 cluster, the 4.x upgrade is a rolling binary swap plus kafka-features.sh. What breaks is mostly what connects to the cluster:
- Java. Clients and Kafka Streams need Java 11 or later. Brokers, Connect, and tools need Java 17. The 4.4 Java page lists Java 17, 21, and 25 as fully supported and recommends the newest LTS.
- Old protocol versions. Brokers must be 2.1 or later before Java clients go to 4.0, and Java clients (including Connect and Streams) must be 2.1 or later before brokers go to 4.0. Non-Java clients need their own check against KIP-896.
- Removed configs and tools.
log.message.format.version,message.format.version,offsets.commit.required.acks, anddelegation.token.master.keyare gone, and so is MirrorMaker 1. - Logging. Log4j moved to Log4j2. The notes point to
log4j-transform-clito convert existing Log4j files. - Group and transaction protocols. The new consumer rebalance protocol (KIP-848) and the transactions server-side defense (KIP-890) turn on when the 4.0 upgrade is finalized. Once consumer groups use the new protocol, the cluster can only be downgraded to 3.4.1 or newer.
# On 3.9, before the 4.x roll: are old protocol versions still in use?
bin/kafka-run-class.sh org.apache.kafka.tools.JmxTool \
--jmx-url service:jmx:rmi:///jndi/rmi://broker-0:9999/jmxrmi \
--object-name 'kafka.network:type=RequestMetrics,name=DeprecatedRequestsPerSec,*' \
--one-time trueInterpretation: KIP-896 added DeprecatedRequestsPerSec in 3.7, tagged by request, API version, and client software name and version. KIP-896 says that if it is 0, "the upgrade is safe." A non-zero value names the client library to upgrade. Check every broker over a full business cycle, because batch jobs and old CDC consumers may connect only once a day.
# After every node runs 4.x and the cluster looks healthy
bin/kafka-features.sh --bootstrap-server broker-0:9092 describe
bin/kafka-features.sh --bootstrap-server broker-0:9092 upgrade --release-version 4.4Interpretation: describe shows the finalized metadata.version and kraft.version. The upgrade raises them. The 4.0 notes say metadata downgrade is not supported because 4.0 carries metadata changes, so this is the second one-way step in the project.
What changes for Debezium and Kafka Connect pipelines?
Kafka Connect workers are Kafka clients. They reach the cluster through bootstrap.servers and never through ZooKeeper, so the migration itself needs no Connect config change. Connect keeps connector offsets, configs, and status in its three internal topics. Debezium keeps its database schema history in a topic as well. The migration moves cluster metadata only, so those topics and their data stay where they are.
What the pipeline does feel is the broker rolls. Each restart moves partition leaders, and Connect's clients retry. Source connectors deliver at least once unless exactly.once.source.support is enabled, so a task restart between offset flushes can repeat a few change events. Keep sinks idempotent, for example with upserts keyed on the primary key.
# Before each roll: record Debezium's committed source offsets (Connect 3.5+)
curl -s http://connect-0:8083/connectors/inventory-cdc/offsets | jq .
curl -s http://connect-0:8083/connectors/inventory-cdc/status | jq '.tasks[].state'Interpretation: the offsets response holds the binlog position, LSN, or resume token per source partition. If a connector comes back after a roll at an older position than the one recorded, you will see re-delivery. The Connect user guide documents PATCH and DELETE on the same endpoint, which only work while the connector is stopped with PUT /connectors/{name}/stop.
Moving the Connect workers to 4.x
You can upgrade brokers first and keep Connect workers on a 3.x release, since 3.x clients are newer than 2.1. When the workers move to 4.x, check these:
- Debezium version. The Debezium 3.7 release notes say 3.7.1.Final (6 November 2026) "has been built against Kafka Connect 4.3.1 and has been tested with version 4.3.1 of the Kafka brokers". The Debezium releases page lists Java 17+ for connectors and Kafka Connect 3.1 and later. Debezium 2.7 lists Java 11 and Connect 2.x and 3.x, so 4.x workers are outside its tested range.
- Removed SMT options.
whitelistandblacklistwere removed fromReplaceField; useincludeandexclude. - Removed task APIs.
SinkTask.onPartitionsRevoked(Collection),onPartitionsAssigned(Collection), andSourceTask.commitRecord(SourceRecord)are gone. Custom or older third-party connectors that override them need a newer build. - Producer defaults. The default
linger.mschanged from 0 to 5 in 4.0. If end-to-end latency matters, setproducer.linger.msin the worker config and measure. - MirrorMaker. MirrorMaker 1 is removed, so CDC topics copied to a second cluster need MirrorMaker 2. Its
topics.blacklist,groups.blacklist, andconfig.properties.blacklistoptions become*.exclude.
# Find connector configs that use removed ReplaceField or MirrorMaker 2 options
for c in $(curl -s http://connect-0:8083/connectors | jq -r '.[]'); do
curl -s "http://connect-0:8083/connectors/$c/config" \
| jq -r --arg c "$c" 'to_entries[]
| select(.key | test("whitelist|blacklist"))
| "\($c): \(.key)"'
doneInterpretation: every hit is a config to edit before it runs on a 4.x worker, because the removed key no longer applies its filter. MirrorMaker 2 running in dedicated mode reads mm2.properties instead, so grep that file too.
What should you monitor during and after the migration?
ZooKeeper-mode dashboards go quiet after the migration. The KRaft vs ZooKeeper page lists the controller metrics that exist only in ZooKeeper mode, such as ControllerChannelManager queue metrics and ControllerStats rate metrics. During the migration, the 3.9 monitoring docs give you a state gauge to track.
| Metric | What to watch |
|---|---|
kafka.controller:type=KafkaController,name=ZkMigrationState | 2 = PRE_MIGRATION, 1 = MIGRATION, 3 = POST_MIGRATION (0 = created in KRaft, 4 = ZooKeeper controller) |
kafka.controller:type=KafkaController,name=MigratingZkBrokerCount | Brokers still in ZooKeeper mode; 0 before you finalize |
kafka.controller:type=KafkaController,name=ActiveControllerCount | Exactly one node across the cluster reports 1 |
kafka.server:type=raft-metrics,name=current-leader | The quorum leader ID; -1 means unknown |
kafka.server:type=raft-metrics,name=commit-latency-max | Time to commit a metadata record; alert on sustained growth |
kafka.server:type=broker-metadata-metrics,name=last-applied-record-lag-ms | How far each broker's metadata view trails the log |
# Quorum health from the command line (3.9 and 4.x)
bin/kafka-metadata-quorum.sh --bootstrap-server broker-0:9092 describe --status
bin/kafka-metadata-quorum.sh --bootstrap-server broker-0:9092 describe --replicationInterpretation: --status summarizes the quorum and --replication lists each voter's and observer's log end offset and lag. Run both before and after every controller restart.
Checking the KRaft quorum after the migration
Illustrative output, abbreviated, shaped like the sample in the Kafka KRaft docs
What healthy looks like: one leader, every controller listed as a voter, every broker listed as an observer, and follower lag near 0. The real output also prints each node's directoryId and endpoints; the field list matches the KRaft operations docs.
On the Connect side, watch task state and Debezium's MilliSecondsBehindSource metric. A lag spike during a broker roll is expected; a task stuck in FAILED afterwards is not. For Connect metrics, see Kafka Connect: JDBC source, SMTs, dead letter queues, and connector monitoring.
Frequently asked questions
Can I upgrade a ZooKeeper-based Kafka cluster directly to Kafka 4.0?
No. Kafka 4.0 and later run only in KRaft mode. A ZooKeeper-mode cluster must first upgrade to the 3.9 bridge release (3.9.1 or later, with inter.broker.protocol.version set to 3.9), migrate its metadata to a KRaft controller quorum, and finalize that migration. Only then can it roll to a 4.x release such as 4.4.0.
Can I roll back a ZooKeeper to KRaft migration?
Yes, until you finalize it. The Kafka 3.9 documentation gives a revert procedure for each completed step: deprovision the controllers, fix the ZooKeeper controller epoch, delete the /controller and /migration znodes, and roll the brokers back to ZooKeeper settings. After the controllers leave migration mode, reverting is not possible, so burn in under dual-write first.
How many KRaft controllers does a production Kafka cluster need?
Three or five dedicated controllers. A majority must be alive, so three controllers tolerate one failure and five tolerate two. Kafka's documentation advises against combined broker and controller nodes in critical environments, and estimates that 5 GB of memory and 5 GB of metadata log disk is sufficient for a typical cluster.
Does the KRaft migration affect Debezium connector offsets?
Not directly. Kafka Connect stores Debezium's source offsets, configs, and status in Kafka topics, and the migration moves only cluster metadata. Broker restarts during the migration can cause connector task retries and, with at-least-once delivery, some duplicate change events. Record offsets before each roll and keep sinks idempotent.
Key takeaways
- ZooKeeper clusters go 3.9.x (3.9.1 or later, IBP 3.9), then the migration, then 4.x. Kafka 4.4.0 is the newest release as of November 2026.
- Run 3 or 5 dedicated controllers with node IDs no broker uses, one per failure domain.
- Two steps are one-way: finalizing the migration and finalizing the 4.x metadata version. Burn in before each.
- Before the 4.x roll, check
DeprecatedRequestsPerSec, move brokers and Connect workers to Java 17, and replace MirrorMaker 1. - Connect and Debezium offsets survive the migration as ordinary topics. Plan for retries and duplicates during rolls, and confirm Debezium supports your Connect worker version.
Plan your Kafka KRaft migration with JusDB
JusDB's Debezium CDC team can review your ZooKeeper-mode cluster and the Connect pipelines on it. We can plan the controller quorum and write a migration runbook with the revert steps for each phase. We can also check your connectors, SMTs, and clients against the 4.x changes before the upgrade.
For the wider cutover, our database migration service can help plan and run the change windows alongside your team. Bring your broker and Connect versions, the connector inventory, and your maintenance windows. Your team approves every production change.