Apache Kafka ZooKeeper
Apache Kafka is a popular platform for building real-time data pipelines and streaming applications. A Kafka cluster is made of several servers (brokers) that must agree on who is in the cluster, which broker leads each partition and how topics are configured. For most of Kafka's history, that coordination job was done by Apache ZooKeeper.
This tutorial explains what ZooKeeper is, exactly what it does for Kafka, how to explore Kafka's data inside ZooKeeper (including from Java), why Kafka replaced it with KRaft, and how to tell which mode your cluster uses.
1. What Is ZooKeeper?
Apache ZooKeeper is a separate open-source service for coordinating distributed systems. It stores small pieces of data in a tree of nodes called znodes, similar to folders and files, and keeps that data identical on several servers. Applications use it for things that are hard to get right on their own: knowing which servers are alive, electing a single leader, and sharing configuration.
- Znodes: each node has a path (for example
/brokers/ids/1) and a small value. - Ephemeral znodes: nodes that disappear automatically when the client that created them disconnects. Kafka uses them to detect failed brokers.
- Watches: clients can ask to be notified when a znode changes, so they react immediately.
- Ensemble: ZooKeeper runs on several servers that agree on every change by majority vote.
2. Role of ZooKeeper in Kafka
In ZooKeeper-based Kafka (versions before 4.0), ZooKeeper acts as the cluster's source of truth for metadata, the information about the cluster rather than the messages themselves:
| Responsibility | What ZooKeeper does |
|---|---|
| Broker membership | Each broker registers itself as an ephemeral znode. If a broker dies, its znode disappears and the cluster learns about it. |
| Controller election | Brokers compete to create the /controller znode. The winner becomes the controller that manages partition leaders. |
| Topic and partition metadata | Stores which topics exist, how many partitions they have and which brokers hold the replicas. |
| Leader and ISR information | Stores the current leader and in-sync replicas of each partition. |
| Configuration | Stores topic, broker and client overrides (for example retention for one topic). |
| Security data | Stores ACLs (who may read or write which topic) and SCRAM user credentials. |
| Change notifications | Uses watches to tell the controller and brokers about changes immediately. |
3. What ZooKeeper Does Not Store
A common misunderstanding is that ZooKeeper stores Kafka messages or consumer progress. It does not:
- Messages: all records live in partition log files on the brokers' disks. ZooKeeper never sees them.
- Consumer offsets: since Kafka 0.9, consumer groups commit offsets to the internal Kafka topic
__consumer_offsets. Only the very old Scala consumer kept offsets in ZooKeeper. - Producer and consumer connections: modern clients connect only to brokers using
bootstrap.servers. They never talk to ZooKeeper.
This is why ZooKeeper data is small (megabytes, not gigabytes) and why it could later be replaced without changing client applications. See Kafka Offset for how offsets work.
4. How Kafka and ZooKeeper Work Together
[ ZooKeeper ensemble: zk1, zk2, zk3 ]
stores metadata, elects the controller
^ ^ ^
| | |
Broker 1 Broker 2 Broker 3
(controller)
^ ^ ^
| | |
Producers and consumers (connect to brokers only)
Kafka keeps its metadata in ZooKeeper under a set of well-known paths:
| Znode path | What it contains |
|---|---|
/brokers/ids/<id> | One ephemeral znode per live broker, with its host, port and rack |
/brokers/topics/<topic> | Partitions of the topic and the brokers that hold their replicas |
/brokers/topics/<topic>/partitions/<n>/state | Current leader and in-sync replicas of one partition |
/controller | Ephemeral znode naming the current controller broker |
/controller_epoch | A counter that increases with every new controller |
/config/topics/<topic> | Topic-level configuration overrides |
/admin/delete_topics | Topics waiting to be deleted |
/cluster/id | The unique cluster ID |
5. Controller Election Step by Step
One broker in the cluster acts as the controller: it decides partition leaders and pushes metadata to the other brokers. ZooKeeper makes sure there is exactly one:
- When brokers start, each tries to create the ephemeral znode
/controller. - ZooKeeper allows only one to succeed. That broker becomes the controller; the others set a watch on
/controller. - If the controller crashes or loses its ZooKeeper session, its ephemeral znode is deleted automatically.
- The watches fire, the remaining brokers race again, and a new controller is elected.
/controller_epochincreases so old controllers are ignored. - The new controller reads all metadata from ZooKeeper and chooses new partition leaders where needed.
On large clusters, step 5 could take a long time because the new controller had to reload the metadata of every partition. This was one of the main reasons for creating KRaft.
6. ZooKeeper Ensemble and Quorum
In production, ZooKeeper runs as an ensemble of an odd number of servers. A change is accepted only when a majority (quorum) agrees, so the ensemble keeps working as long as most servers are alive:
| ZooKeeper servers | Majority needed | Servers that can fail |
|---|---|---|
| 1 | 1 | 0 (development only) |
| 3 | 2 | 1 |
| 5 | 3 | 2 |
| 4 | 3 | 1 (no better than 3, so avoid even numbers) |
If the ensemble loses its majority, Kafka brokers keep serving existing partitions for a while, but no metadata changes (new leaders, new topics) can happen until ZooKeeper recovers.
7. Example: Running Kafka with ZooKeeper
These commands work with Kafka 3.x downloads, which still include ZooKeeper. Kafka 4.0 and later no longer ship zookeeper-server-start.sh.
# 1. Start ZooKeeper (listens on port 2181 by default)
bin/zookeeper-server-start.sh config/zookeeper.properties
# 2. In another terminal, start a Kafka broker that uses this ZooKeeper
bin/kafka-server-start.sh config/server.properties
# 3. Create a topic named "orders" (tools talk to the broker, not to ZooKeeper)
bin/kafka-topics.sh --create --topic orders \
--bootstrap-server localhost:9092 --partitions 3 --replication-factor 1
# 4. Check the topic
bin/kafka-topics.sh --describe --topic orders --bootstrap-server localhost:9092
The broker finds ZooKeeper through these settings in server.properties:
# Unique ID of this broker
broker.id=1
# ZooKeeper ensemble; the optional "/kafka" part (chroot) keeps Kafka data in its own folder
zookeeper.connect=zk1:2181,zk2:2181,zk3:2181/kafka
# How long ZooKeeper waits before treating a silent broker as dead
zookeeper.session.timeout.ms=18000
listeners=PLAINTEXT://:9092
log.dirs=/var/lib/kafka/data
kafka-topics.sh --zookeeper localhost:2181. That option was removed in Kafka 3.0; always use --bootstrap-server instead.8. Exploring Kafka Data in ZooKeeper
Kafka 3.x includes zookeeper-shell.sh, which lets you look inside ZooKeeper. This is useful for learning and troubleshooting; never edit Kafka's znodes by hand.
# List the IDs of live brokers
bin/zookeeper-shell.sh localhost:2181 ls /brokers/ids
# Show which broker is the controller
bin/zookeeper-shell.sh localhost:2181 get /controller
# Show the leader and in-sync replicas of partition 0 of "orders"
bin/zookeeper-shell.sh localhost:2181 get /brokers/topics/orders/partitions/0/state
[1, 2, 3]
{"version":2,"brokerid":1,"timestamp":"1767225600000"}
{"controller_epoch":4,"leader":2,"version":1,"leader_epoch":7,"isr":[2,3,1]}
The output shows three live brokers, broker 1 as the controller, and broker 2 leading partition 0 with all three replicas in sync.
9. Reading Kafka Metadata from Java
The official ZooKeeper Java client can read the same znodes. This program connects to ZooKeeper, lists live brokers and prints the current controller. It is a learning example; real applications should use Kafka's Admin client instead (see section 12).
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.8.4</version>
</dependency>
package com.example.kafka;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
public class KafkaZooKeeperReader {
public static void main(String[] args) throws Exception {
CountDownLatch connected = new CountDownLatch(1);
// Wait until the session is established before reading
ZooKeeper zk = new ZooKeeper("localhost:2181", 15_000, event -> {
if (event.getState() == Watcher.Event.KeeperState.SyncConnected) {
connected.countDown();
}
});
try {
if (!connected.await(10, TimeUnit.SECONDS)) {
throw new IllegalStateException("Could not connect to ZooKeeper");
}
List<String> brokerIds = zk.getChildren("/brokers/ids", false);
System.out.println("Live brokers: " + brokerIds);
for (String id : brokerIds) {
byte[] data = zk.getData("/brokers/ids/" + id, false, null);
System.out.println(" broker " + id + " -> "
+ new String(data, StandardCharsets.UTF_8));
}
byte[] controller = zk.getData("/controller", false, null);
System.out.println("Controller: "
+ new String(controller, StandardCharsets.UTF_8));
} finally {
zk.close();
}
}
}
Live brokers: [1, 2, 3]
broker 1 -> {"host":"kafka1","port":9092,"rack":null,...}
broker 2 -> {"host":"kafka2","port":9092,"rack":null,...}
broker 3 -> {"host":"kafka3","port":9092,"rack":null,...}
Controller: {"version":2,"brokerid":1,"timestamp":"1767225600000"}
10. Limitations of ZooKeeper
ZooKeeper served Kafka well for over a decade, but it brought real problems as clusters grew:
- Two systems to run: teams had to deploy, secure, monitor and upgrade both Kafka and a ZooKeeper ensemble, each with its own configuration and skills.
- Slow controller failover: a new controller had to reload all metadata from ZooKeeper, which could take minutes on clusters with hundreds of thousands of partitions.
- Limited scale: the number of partitions per cluster was practically limited to around 200,000.
- Metadata could get out of sync: the controller's in-memory view and ZooKeeper's data could briefly disagree, causing tricky bugs.
- Separate security setup: ZooKeeper needed its own authentication and TLS configuration, in addition to Kafka's.
11. ZooKeeper vs KRaft
KRaft (Kafka Raft) moves metadata management into Kafka itself. A small group of Kafka controller nodes store metadata in an internal, replicated log and elect a leader with the Raft protocol.
| Aspect | ZooKeeper mode | KRaft mode |
|---|---|---|
| Where metadata lives | In a separate ZooKeeper ensemble | In Kafka's own replicated metadata log |
| Systems to operate | Two (Kafka + ZooKeeper) | One (Kafka only) |
| Controller | One broker elected through ZooKeeper | A quorum of 3 or 5 controllers using Raft |
| Controller failover | Slow on large clusters (reloads all metadata) | Fast (standby controllers already have the metadata) |
| Scale | Around 200,000 partitions | Millions of partitions |
| Broker setting | zookeeper.connect | process.roles and controller.quorum.voters |
| Status | Deprecated in 3.5, removed in Kafka 4.0 | Production-ready since 3.3, the only mode in 4.0+ |
For a detailed comparison, see Kafka ZooKeeper vs KRaft.
12. Is My Cluster Using ZooKeeper or KRaft?
Applications do not need to know, but operators often do. Kafka's Admin client can tell you: the describeMetadataQuorum() call only works on KRaft clusters. On a ZooKeeper-based cluster it fails with an UnsupportedVersionException.
package com.example.kafka;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
import org.apache.kafka.clients.admin.Admin;
import org.apache.kafka.clients.admin.AdminClientConfig;
import org.apache.kafka.clients.admin.QuorumInfo;
import org.apache.kafka.common.Node;
import org.apache.kafka.common.errors.UnsupportedVersionException;
public class KafkaModeCheck {
public static void main(String[] args) throws Exception {
Properties props = new Properties();
props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
try (Admin admin = Admin.create(props)) {
Node controller = admin.describeCluster().controller().get();
System.out.println("Active controller id: " + controller.id());
try {
QuorumInfo quorum = admin.describeMetadataQuorum().quorumInfo().get();
System.out.println("Mode: KRaft");
System.out.println("Quorum leader: " + quorum.leaderId()
+ ", voters: " + quorum.voters().size());
} catch (ExecutionException e) {
if (e.getCause() instanceof UnsupportedVersionException) {
// The metadata quorum API does not exist in ZooKeeper mode
System.out.println("Mode: ZooKeeper");
} else {
throw e;
}
}
}
}
}
# On a ZooKeeper-based Kafka 3.x cluster
Active controller id: 1
Mode: ZooKeeper
# On a KRaft cluster
Active controller id: 3002
Mode: KRaft
Quorum leader: 3002, voters: 3
From the command line, the same check is bin/kafka-metadata-quorum.sh --bootstrap-server localhost:9092 describe --status, which only succeeds on KRaft clusters.
13. Migrating from ZooKeeper to KRaft
Because Kafka 4.0 has no ZooKeeper support, every ZooKeeper-based cluster must move to KRaft before upgrading to 4.0. Kafka supports an online migration with no downtime for clients:
- Upgrade to a recent 3.x release (3.9 is the final "bridge" release and the recommended starting point).
- Deploy a KRaft controller quorum (usually 3 controllers) configured in migration mode.
- Enable migration on the brokers and restart them one by one. The controllers copy all metadata from ZooKeeper.
- Run in dual-write mode: the KRaft controllers manage the cluster while still writing metadata back to ZooKeeper, so you can roll back if needed.
- Finalize: restart the brokers in pure KRaft mode and remove the ZooKeeper settings.
- Decommission ZooKeeper, then upgrade to Kafka 4.x.
Producers and consumers keep working during the whole process because they only ever talk to brokers.
14. Best Practices
| Do | Avoid |
|---|---|
| Use KRaft for every new cluster | Starting new projects on ZooKeeper-based Kafka |
| Run 3 or 5 ZooKeeper servers on separate machines (existing clusters) | A single ZooKeeper server or an even number of servers |
Use a chroot such as /kafka in zookeeper.connect | Sharing the ZooKeeper root with other applications |
| Give ZooKeeper fast, dedicated disks for its transaction log | Putting ZooKeeper data on busy shared disks |
| Secure ZooKeeper with SASL authentication and TLS | Leaving ZooKeeper open on the network |
Connect tools with --bootstrap-server | Using the removed --zookeeper option |
| Plan the migration to KRaft before upgrading to Kafka 4.0 | Editing Kafka znodes by hand |
15. Summary
For most of its history, Kafka relied on ZooKeeper as its coordinator: brokers registered there, the controller was elected through it, and topic, partition, configuration and security metadata were stored in it. ZooKeeper never held messages or modern consumer offsets, and clients never connected to it directly. As clusters grew, running two systems and slow controller failover became real problems, so Kafka introduced KRaft, which keeps metadata inside Kafka itself. ZooKeeper mode was deprecated in Kafka 3.5 and removed in 4.0. Today, ZooKeeper knowledge is still useful for running and migrating older clusters, while all new Kafka clusters should use KRaft.
Frequently Asked Questions (FAQ)
Q1. What is ZooKeeper in Kafka?
ZooKeeper is a separate coordination service that older Kafka versions used to manage cluster metadata: which brokers are alive, which broker is the controller, and how topics and partitions are configured. Kafka 4.0 and later no longer use it.
Q2. Why did Kafka need ZooKeeper?
A distributed system needs one reliable place to agree on membership, leadership and configuration. When Kafka was created, ZooKeeper was a proven tool for exactly this, so Kafka used it instead of building its own consensus system.
Q3. Does Kafka still need ZooKeeper?
No. Since Kafka 3.3, KRaft mode is production-ready, and Kafka 4.0 removed ZooKeeper support completely. Only older 2.x and 3.x clusters running in ZooKeeper mode still need it.
Q4. Which Kafka version removed ZooKeeper?
Kafka 4.0. ZooKeeper mode was marked as deprecated in Kafka 3.5, and 3.9 is the last version that supports it and the bridge release for migration.
Q5. What is KRaft?
KRaft (Kafka Raft) is Kafka's built-in metadata system. A quorum of Kafka controller nodes stores metadata in an internal replicated log and elects a leader using the Raft consensus protocol, so no external ZooKeeper is needed.
Q6. Does ZooKeeper store Kafka messages?
No. Messages are stored only in partition log files on the Kafka brokers. ZooKeeper held only small metadata, such as broker lists, partition leaders and configurations.
Q7. Are consumer offsets stored in ZooKeeper?
Not in modern Kafka. Since version 0.9, consumer groups commit offsets to the internal topic __consumer_offsets on the brokers. Only the old, long-removed Scala consumer stored offsets in ZooKeeper.
Q8. Do producers and consumers connect to ZooKeeper?
No. Modern Kafka clients connect only to brokers through bootstrap.servers:
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
That is why migrating from ZooKeeper to KRaft does not require any client code changes.
Q9. What is the Kafka controller?
The controller is the component that manages the cluster's metadata: it chooses partition leaders, tracks in-sync replicas and tells brokers about changes. In ZooKeeper mode it is one broker elected through ZooKeeper; in KRaft mode it is a quorum of controller nodes.
Q10. How does ZooKeeper elect the Kafka controller?
Every broker tries to create the ephemeral znode /controller. ZooKeeper lets only one succeed, and that broker becomes the controller. If it fails, the znode disappears, the other brokers are notified through watches, and a new election happens.
Q11. What is a znode?
A znode is a data node in ZooKeeper's tree, identified by a path such as /brokers/ids/1. It holds a small value and may have child znodes. Ephemeral znodes are deleted automatically when the client that created them disconnects.
Q12. What is a ZooKeeper ensemble?
An ensemble is a group of ZooKeeper servers that keep identical copies of the data. Changes are accepted only when a majority agrees, so an ensemble of 3 survives 1 failure and an ensemble of 5 survives 2.
Q13. Why should a ZooKeeper ensemble have an odd number of servers?
Because a majority is needed. Four servers need 3 to agree and can lose only 1, the same as three servers, so the fourth server adds cost without adding fault tolerance.
Q14. What happens if ZooKeeper goes down?
Brokers can keep serving existing partitions for a while, but no metadata changes can happen: no new leaders after a broker failure, no new topics and no configuration changes. If a broker also fails during that time, its partitions may stay unavailable until ZooKeeper returns.
Q15. What is zookeeper.connect?
It is the broker setting that lists the ZooKeeper servers, for example zk1:2181,zk2:2181,zk3:2181/kafka. The optional /kafka at the end is a chroot that keeps Kafka's znodes in their own folder.
Q16. Why was the --zookeeper option removed from kafka-topics.sh?
Tools that write directly to ZooKeeper bypass the broker's checks and security. Since Kafka 3.0, all admin tools must use --bootstrap-server, which goes through the brokers like any other client.
Q17. How can I check whether my cluster uses ZooKeeper or KRaft?
Run kafka-metadata-quorum.sh --bootstrap-server <broker> describe --status, or call Admin.describeMetadataQuorum() from Java. Both succeed only on KRaft clusters. You can also check the broker configuration for zookeeper.connect (ZooKeeper mode) or process.roles (KRaft mode).
Q18. Can I migrate from ZooKeeper to KRaft without downtime?
Yes. Kafka supports an online migration: upgrade to a recent 3.x release, add KRaft controllers in migration mode, roll the brokers, let the controllers copy the metadata, then finalize in pure KRaft mode. Clients keep working throughout.
Q19. Is ZooKeeper used only by Kafka?
No. ZooKeeper is a general-purpose coordination service used by many other systems, for example Apache HBase, Apache Solr (SolrCloud) and Apache Hadoop's high-availability features.
Q20. Should I still learn ZooKeeper for Kafka?
Learn the basics: many companies still run Kafka 2.x or 3.x clusters on ZooKeeper, migrations need ZooKeeper knowledge, and it is a common interview topic. For new projects, focus on KRaft, which is the only mode in current Kafka versions.