Why the standard Kafka operator built its own pod controller instead of using StatefulSets, how ZooKeeper finally got removed from Kafka itself, and the real tradeoffs worth knowing before running this in production.
If the mental picture of Kafka on Kubernetes still involves a three-node ZooKeeper StatefulSet sitting next to a Kafka broker StatefulSet, that picture is out of date on two separate fronts at once.
ZooKeeper is gone from Kafka itself. And Strimzi, the standard operator for running Kafka on Kubernetes, does not manage its pods with a StatefulSet at all. Both changes happened for real engineering reasons, and understanding them explains more about what actually matters when running a stateful system like Kafka in Kubernetes than either fact does on its own.
ZooKeeper Is Actually Gone Now
Kafka relied on Apache ZooKeeper for cluster coordination and metadata since it was first built, tracking which broker was the controller, which partitions existed, and who was allowed to lead them. Running Kafka meant running and operating an entirely separate distributed system just to keep Kafka itself coordinated.
KRaft, short for Kafka Raft, replaced that dependency by moving metadata management directly into Kafka using the Raft consensus algorithm. It was introduced in Kafka 3.3, reached production readiness in Kafka 3.5, and as of Kafka 4.0, ZooKeeper support has been removed entirely. Every new Kafka cluster today runs in KRaft mode by default, with dedicated controller nodes handling metadata and consensus instead of an external ZooKeeper ensemble.
This matters beyond simplification. Controller failover under KRaft happens within seconds instead of the longer recovery window ZooKeeper-based clusters were known for, and there is one fewer distributed system to monitor, patch, and keep highly available
Why Strimzi Does Not Use StatefulSets
Strimzi is a CNCF project and the standard way to run Kafka on Kubernetes, turning a Kafka cluster into a set of custom resources that the operator watches and reconciles continuously. For years, it created those broker and controller pods using a native Kubernetes StatefulSet, the same primitive most Kubernetes tutorials point to for any stateful workload.
According to Strimzi's own engineering proposal for the change, StatefulSets carry three specific limitations that caused real problems for a Kafka cluster. Pods can only be created or removed based on their index number, meaning if broker two specifically becomes unstable, for instance because a bad topic is causing it problems, there is no way to delete just that one pod and replace it. Scaling down a StatefulSet can only ever remove the highest-numbered pod. Every pod in a StatefulSet is also expected to share close to identical configuration, and changing storage configuration for pods already running in a StatefulSet is difficult to do safely.
Strimzi's answer, introduced in version 0.29 and now the default, is a custom resource called StrimziPodSet. Instead of generating pods from one shared template, it manages full, individually named pod definitions directly, which is what makes per-broker configuration differences and targeted pod replacement possible in the first place.
What the Operator Actually Automates
The Cluster Operator is the core of Strimzi. It watches a Kafka custom resource describing the desired cluster, brokers, controllers, storage, listeners, and continuously reconciles the running cluster to match it. Under KRaft mode, brokers and controllers are represented as KafkaNodePool resources, which is also what makes it possible to run controller-only nodes and broker-only nodes with different resource profiles instead of every pod being forced into the same shape.
The same Cluster Operator also manages several optional pieces that turn a bare Kafka cluster into something closer to a complete platform: Kafka Connect for moving data in and out of Kafka, MirrorMaker for replicating between clusters, the Kafka Bridge for HTTP access, Cruise Control for automated rebalancing, and Kafka Exporter for consumer lag metrics. None of these are separate systems to install by hand. They are all declared the same way, as fields on a custom resource that the Cluster Operator watches.
A second component, the Entity Operator, handles day-to-day operational objects that would otherwise require manually running Kafka's own command-line tools inside a pod. Its Topic Operator manages KafkaTopic resources, and its User Operator manages KafkaUser resources and their access control, both declared as ordinary Kubernetes YAML instead of imperative commands run against a live cluster.
| What a StatefulSet provides | What Strimzi adds on top |
|---|---|
| Stable pod naming and volume reattachment | Per-broker configuration differences via KafkaNodePool |
| Ordered, index-based pod creation | Targeted replacement of a specific unhealthy broker |
| A generic pattern for any stateful workload | Declarative topic and user management through CRDs |
| No awareness of Kafka-specific state | Coordinated rolling upgrades across brokers and controllers |
The Real Tradeoff Nobody Puts in the Quickstart
A native StatefulSet is managed by the Kubernetes control plane itself. If a pod in a StatefulSet dies, Kubernetes recreates it independently of any custom software being alive and healthy at that moment.
A StrimziPodSet is not. Strimzi's own maintainers have confirmed directly that this is a genuine drawback: since StrimziPodSets are reconciled by the Strimzi Operator rather than Kubernetes itself, the Strimzi Operator instance becomes a critical piece of the system. If it is down, unhealthy, or missed a notification during something like a node drain, pods will not be recreated until the operator is healthy again and periodic reconciliation catches the drift.
Most Strimzi deployments run a single operator replica by default. That is a fundamentally different failure mode from a StatefulSet, where pod recreation keeps working even if every custom controller in the cluster is offline. It does not make Strimzi unreliable. It means the operator's own availability now sits directly on the path to Kafka's self-healing, which is worth knowing before assuming the operator handles everything the same way Kubernetes itself would.
Where Production Pain Actually Shows Up
Storage decisions that are hard to undo
Kafka brokers are disk-heavy, and the storage choice made at cluster creation is not easy to walk back. Under-provisioning disk means running into space issues as topics retain data; switching storage classes or shrinking volumes after the fact is disruptive in a way that is easy to underestimate before it happens.
Partition rebalancing does not fix itself, mostly
Adding a new broker to a Kafka cluster does not automatically redistribute existing partitions onto it. Without an active rebalancing strategy, new brokers can sit nearly empty while older ones stay under load, which is precisely the kind of operational task that day-two Kafka administration involves and that a fresh installation does not warn about.
Strimzi ships an integration with Cruise Control specifically to solve this. It builds a workload model from CPU, disk, and network usage across the cluster and generates an optimization proposal, submitted and tracked through a KafkaRebalance custom resource. That proposal has to be reviewed and approved, by setting an annotation on the resource, unless auto-approval is explicitly configured.
Full automation here is newer than it might appear. Strimzi only added automatic rebalancing triggered directly by a scale-up or scale-down event in late 2024. Before that, scaling a cluster and rebalancing it were two separate manual steps: scale first, then run a KafkaRebalance in add-brokers or remove-brokers mode to actually move partitions onto or off the affected nodes. Cruise Control removes the guesswork from deciding what to move. It does not remove the need to know it has to be triggered.
Rack awareness has to be set up deliberately
Kubernetes will happily schedule every Kafka broker onto the same availability zone unless rack awareness and pod anti-affinity are explicitly configured. Nothing prevents that misconfiguration from running successfully for months until an entire zone goes down at once.
Consumer lag stays invisible unless it is wired up on purpose
A Kafka cluster can look perfectly healthy, every broker running, every partition with a leader, while a consumer group quietly falls further and further behind on a topic that matters. Broker-level health checks will not catch this, because from the broker's point of view nothing is wrong. Strimzi includes an optional component called Kafka Exporter specifically to expose consumer lag as a Prometheus metric, but it has to be enabled and wired into an actual dashboard and alert before it does anything. Teams that skip this step usually find out about lag from a downstream complaint, not from monitoring.
JVM heap settings do not automatically respect the container's memory limit
Kafka brokers run on the JVM, and a JVM that is not told about the memory limit of the container it is running in can make heap sizing decisions based on the node's total memory instead. On a shared Kubernetes node, that mismatch is exactly how a broker ends up OOMKilled well before it appears to be under any real memory pressure. This is not unique to Kafka, but it catches teams new to running JVM-based workloads in containers often enough that it belongs on this list. Setting heap sizing relative to the container's memory limit, not the node's, is a five-minute fix once it is known to look for.
Getting Data Between Clusters Is a Separate Problem
Everything covered so far happens inside a single Kafka cluster. Replicating data between two separate Kafka clusters, whether for disaster recovery, migrating regions, or feeding a downstream analytics cluster, is not something the Cluster Operator does automatically. It requires deploying MirrorMaker 2 as its own component, configured through a dedicated KafkaMirrorMaker2 custom resource.
MirrorMaker 2 automatically synchronizes topic configuration between the source and target clusters, so new topics and partition changes on one side get created on the other without manual intervention. It also solved a real limitation in the original MirrorMaker: consumer offsets on the source topic and the replicated topic used to end up at completely different positions, which made failing over to the replicated cluster during an actual incident harder than it should have been. MirrorMaker 2 handles offset translation directly, which is the difference between a mirrored cluster that is actually usable during a failover and one that only looks like a backup.
The pattern here matches what came up with CloudNativePG. Automated recovery inside one cluster is a solved, well-tested problem. Getting from one cluster to another, for disaster recovery or migration, is a separate piece of infrastructure that has to be deployed and tested on its own, not something that comes free with the base setup.
Should You Actually Run Kafka on Kubernetes Yourself?
Managed Kafka services, Confluent Cloud, Amazon MSK, and similar offerings solve the same coordination and scaling problems using a team whose entire job is operating Kafka reliably. Running it inside Kubernetes with Strimzi instead means taking a real share of that operational responsibility back in-house: storage planning, rebalancing, rack awareness, consumer lag monitoring, and now the operator's own availability as well.
That trade makes sense in a fairly specific set of situations. A platform team already running Kubernetes at meaningful scale, with real operational experience running other stateful workloads through operators, gets genuine value from provisioning Kafka the same declarative way as everything else in the cluster instead of managing it through a separate console. It also makes sense where event data needs to stay inside an existing Kubernetes network boundary for latency or compliance reasons that a managed service outside that boundary cannot satisfy.
Outside of those cases, a managed service is often the simpler and safer starting point, particularly for a team without existing Kafka operational experience or without the time to build monitoring for consumer lag and rebalancing before the first real incident forces the issue. Kafka is not a system that forgives being run casually, on Kubernetes or anywhere else, and Strimzi does not change that fact. It changes how the operational work gets expressed, from imperative commands to declarative custom resources, not how much of it there is.
The Pattern Worth Noticing
This is the second time the same shape of story has shown up: a serious, production-grade Kubernetes operator quietly moves away from the primitive most tutorials present as the default answer, because that primitive was built to be generic and the real workload needed something more specific. CloudNativePG did it for Postgres. Strimzi did it for Kafka. Both moves also introduced the same honest tradeoff in different clothes, the automation is only as reliable as the operator running it.
Running a stateful system on Kubernetes well means understanding that tradeoff going in, not discovering it the first time the operator itself has a bad day.

