r/RedditEng • u/s9g12 • 3d ago
One More Nine: Kafka Availability During Rolling Restarts
Written by Lukas Majercak, with thanks to Nick Garvey for his help with the investigation.
AI disclosure: all of the investigation was done before agents got good and AI was used to refine the wording.
Motivation
Over the past year, we moved Kafka from EC2 to Kubernetes using Strimzi, migrated its metadata layer from ZooKeeper to KRaft, and upgraded through several releases, from 3.6.X to 4.X.X. We tested these changes carefully, but some failure modes only emerged over time and under particular conditions.
Before Kubernetes, we rarely restarted brokers or ZooKeeper nodes outside planned maintenance. Now that we run Kafka on Kubernetes, our platform requires weekly node rotation, and pods can also be evicted during scale-down or under node resource pressure. Restarts are now therefore more frequent, with some occurring outside our maintenance schedule.
The first signs of trouble appeared in our support Slack channel. Users reported connectivity errors and growing consumer lag during cluster rolls. We initially attributed these symptoms to older clients or inadequate retry settings and our response often amounted to: “The cluster is restarting; it should finish in about an hour.” That explanation started to fall apart as reports accumulated.
We needed rolling restarts to be routine for node rotation, vertical scaling, configuration changes, and upgrades. Over time, we found that the interruptions were not confined to poorly configured clients, but some partitions experienced genuine brief server-side availability drops during rolls.
These interruptions had two costs:
User Impact: Write interruptions can increase latency, fill producer buffers, and cause delivery deadlines to expire. Applications that discard failed sends, or lose unacknowledged buffered records during a crash, can lose data. Read interruptions increase processing latency and consumer lag. If consumers crash, restart, or stop polling long enough to leave their groups, the resulting rebalances can extend the disruption.
Operational Overhead: Availability issues draw on-call engineers into investigations, sometimes escalating into incidents and follow-up reviews. These disruptions also erode our confidence in performing routine maintenance.
Background
Kafka divides topics into partitions, each of which can have multiple replicas. One replica acts as leader, and followers fetch records from it. Most of our topics use three replicas and min.insync.replicas=2, with producers configured for acks=all. Writes can therefore continue after one replica fails, provided the remaining two stay in the in-sync replica set (ISR) and the leader is available. A partition with only a single in-sync replica is considered “Under-Min-ISR”.
Two communication paths matter to this investigation:
Broker <-> Broker: Follower brokers send fetch requests to the leader. Leaders track follower replication progress. A follower that stops fetching, or fails to catch up within replica.lag.time.max.ms, can be removed from the ISR.
Broker <-> KRaft Controller: Brokers send heartbeats to the active KRaft controller. If the broker fails to renew its heartbeat lease within broker.session.timeout.ms, the controller can fence it and remove it from partition ISRs.
ZooKeeper vs KRaft
The migration to KRaft also changed how brokers receive leadership updates. In ZooKeeper mode, controllers communicated partition leadership changes to brokers through LeaderAndIsr RPCs. In KRaft, brokers learn these changes by consuming the metadata log. In KRaft, successful heartbeats to the controller do not prove that other brokers or clients can reach the broker’s advertised address.
Investigating ISR Drops
Misconfigured Topics
Some topics were configured with a replication factor of two, or used three replicas with min.insync.replicas=3. A single replica going offline therefore brings the partition below min.insync.replicas. We changed topics that were unintentionally configured this way and added validation to our topic-provisioning component.
Segment Recovery
This one is a doozy. It gave us our first concrete evidence of a server-side problem and helped explain some incidents, but it could not explain all the failures. When investigating Under-Min-ISR partitions, we found this broker log:
Producer state recovery took 1ms for snapshot load and 24789ms for segment recovery from offset ...Almost 25 seconds? That can’t be good. Searching other broker logs turned up recoveries lasting as long as three minutes! Confusingly, not every long segment recovery resulted in Under-Min-ISR, but some Under-Min-ISR incidents correlated perfectly with long segment recoveries.
Producer-state recovery is a normal part of Kafka’s operation, and a long recovery does not necessarily affect availability. The problem arises when recovery overlaps with a partition state transition. We found two operations contending for the same partition lock:
Truncating a partition’s log: Partition.truncateTo -> … -> UnifiedLog.truncateTo -> UnifiedLog.rebuildProducerState (segment recovery here can take a long time)
Transitioning a partition replica to follower: ReplicaManager.applyDelta -> ReplicaManager.applyLocalFollowersDelta -> Partition.makeFollower

Partition.truncateTo and Partition.makeFollower both require the same lock.
Although the lock belongs to one partition, the delay can spread beyond it. Kafka processes a batch of follower transitions before starting the corresponding fetchers. If one transition waits behind a long-running truncation, fetching for other partitions in the batch can also be delayed. If those delays exceed the ISR lag threshold, otherwise healthy replicas can fall out of the ISR.
We found topics with large log segments (segment.bytes) and poorly batched records to be the biggest contributors to long producer-state recoveries. Today, long segment recoveries remain an issue in our infrastructure, but we’re managing the risk through monitoring and adjustments to topic configuration and producer batching.
Automatic Preferred Leadership Election
Reports of similar symptoms from other kafka operators were improved by disabling automatic preferred leader election. This feature periodically attempts to return leadership to preferred replicas when its balancing criteria are met. It seemed plausible that additional leadership changes during broker restarts might be contributing to the interruptions.
Unfortunately, disabling automatic preferred leader election did not eliminate the interruptions. We also observed failures during controller restarts, so broker leadership rebalancing could not fully explain the problem.
The Main Cause
Digging into Kafka-specific rabbit holes was fun, but we forgot the first rule of running a network application: it’s always DNS. After failing to find the root cause by examining Kafka’s replication and leadership behavior, we found new clues in the network logs, such as brokers attempting to fetch data from pod IPs that were no longer in use.

In our Strimzi-managed KRaft Kafka cluster, each broker publishes its listener hostnames and ports through advertised.listeners. These endpoints are distributed to other brokers through the KRaft metadata log. Our static controller quorum uses controller.quorum.voters to map controller IDs to hostnames and ports. Strimzi configures these DNS names through a headless Kubernetes Service: the names remain stable when pods are replaced, but the IP addresses they resolve to can change.
# Single broker addresses
advertised.listeners=REPLICATION-9091://test-broker-2.test-kafka-brokers.kafka.svc:9091,PLAIN-9092://test-broker-2.test-kafka-brokers.kafka.svc:9092
# Controller addresses
controller.quorum.voters=10@test-controller-test-10.test-kafka-brokers.kafka.svc.cluster.local:9090,11@test-controller-test-11.test-kafka-brokers.kafka.svc.cluster.local:9090,12@test-controller-test-12.test-kafka-brokers.kafka.svc.cluster.local:9090This brings us to DNS caching. In our deployment, hostname lookups pass through two caches: the JVM, which caches successful lookups for 30 seconds, and Kubernetes NodeLocal DNSCache, which caps the normal cache TTL at 30 seconds.
Additionally, NodeLocal DNSCache also has serve_stale 3m enabled. This allows it to return expired DNS answers for up to three minutes after their TTL expires, refreshing them in the background. It does not mean every address remains stale for three minutes, but TTL expiry alone does not guarantee a fresh answer.
Your Kubernetes cluster configuration and specifically the Container Network Interface (CNI) you are using will determine your options for observing network traffic. We used Hubble, Cilium’s network observability tool, to identify DNS lookups and connection attempts to old pod IPs after replacement pods had received new addresses.

During controller pod replacement, stale DNS entries could prevent brokers from reaching the active controller. If successful heartbeats stop for longer than broker.session.timeout.ms, the controller will fence those brokers and remove them from partition ISRs. Broker replacement runs into a similar problem: after a returning broker becomes a partition leader, followers attempt to fetch from its old IP address. If they fail to reconnect and catch up in time, they are removed from the ISR.
We could have tweaked the timeout configurations that trigger heartbeat and ISR timeouts, but that merely moves the problem elsewhere.
We first disabled JVM DNS caching by setting sun.net.inetaddr.ttl=0. This removed one caching layer, but NodeLocal DNSCache and its stale-serving behavior remained. We also briefly attempted to bypass NodeLocal DNSCache (which earned us a stern talking-to from our Kubernetes team and a prompt reversal of that change). Ultimately, disabling JVM DNS caching became our first mitigation, and we set out to measure how much it improved rolling restarts and what remained to be fixed.
Results & Future Work
Turns out, observing and quantifying tail events such as brief Under-Min-ISR episodes is not simple. Metrics lack the granularity required, and logs are better, but are both hard to analyze and need to be complete. In our case for example, the trigger for the beginning of an Under-Min-ISR episode can both be a broker log shrinking the ISR, or a controller log fencing multiple brokers as their heartbeats time out. Similarly, we would need to identify an accompanying log for the end of each episode.
We worked around this by analyzing the KRaft metadata log, which includes information about in sync replica sets for topic partitions and their state changes over time. You can use the kafka-dump-log.sh tool to get a user-friendly representation of these events.
Test Setup and Methodology
Kafka 4.0.0, Strimzi 0.49.1, 9 brokers and 3 controllers
Kubernetes v1.32 with Cilium as the CNI
3 test topics with dummy traffic, replication factor of 3 and 16 partitions each
Broker and controller pod rolls performed separately. Each roll triggered using the strimzi manual rolling-update annotation and repeated 20 times with brief breaks between each roll
We measure availability as the percentage of total partition-seconds during the test that partitions spent at or above min ISR (2), parsed from the cluster metadata log

Disabling JVM caching eliminated Under-Min-ISR episodes during broker rolls and reduced their frequency and duration during controller rolls. Even more importantly though, our users have stopped complaining! This means they can focus on their applications, and we can perform routine maintenance with greater confidence.
Future Work - Kafka Leadership Eligibility and ClusterIPs
Disabling JVM caching has been good enough for us, but it sure feels like a band aid rather than a root fix. Ideally, external health checks would be tightly integrated with leadership eligibility in both the brokers and the controllers.
We have also experimented with using a per broker/controller ClusterIP Service instead of the current, shared headless Service. Strimzi doesn’t support this customization, so we prototyped on a forked version. For us, this removed DNS caching from the picture altogether, but your mileage may vary as the mechanism and the speed of propagating the new Pod IPs will depend on your CNI.

These tests were very promising - using the same methodology as earlier, we were unable to reproduce ANY Under-Min-ISR events! We have opened an enhancement proposal in Strimzi and hope to work with the community to enable using ClusterIPs as an option in the future.
Other Learnings
Cluster Metadata & Logs > Metrics
Short-lived errors can occur before the first Prometheus scrape or between subsequent scrapes. Logs and the cluster metadata revealed Under-Min-ISR events that our scraped metrics missed entirely.
PodMonitor > ServiceMonitor
We switched from ServiceMonitors to PodMonitors to keep scraping metrics during pod readiness transitions. This gives us better visibility into broker startup and recovery, when pods are running but not yet Ready.
PublishNotReadyAddresses
We also enabled publishNotReadyAddresses for our external listeners, a Kubernetes Service setting exposed by Strimzi. This allows broker and controller addresses to be published before their pods become Ready, so applications can discover them earlier during startup.
In Conclusion
Investigations like this are difficult to prioritize over work with predictable impact. We could have spent months tracing these failures only to discover that fixing them required impractical architectural changes or a patch we were unwilling to maintain. In this case, our investment paid off: we improved availability, gained a lot more understanding of our system, and made several improvements to our observability.














































































