Ch. 14 · Kubernetes

Kubernetes Disruption Budgets and Maintenance

How PodDisruptionBudgets gate evictions during drains and upgrades, minAvailable vs maxUnavailable maths, and budgets that block drains.

~6 min readadvancedupdated Oct 7, 2026

A cluster upgrade replaces every node. A node-pool resize removes some. Each of those operations has to move your Pods somewhere else, and without coordination it could move all three replicas of a service at once. A PodDisruptionBudget is how an application owner tells the cluster “you may take my Pods away for maintenance, but not more than this many at a time”. It’s a small object with a surprising number of ways to get it wrong.

This applies to Kubernetes 1.30 onward. The budgets below are valid YAML; the kubectl get pdb and drain output is a realistic sample, not a recording.

Voluntary and involuntary disruption

Kubernetes separates disruptions into two kinds, and PDBs only deal with one.

Involuntary disruptions are things nobody chose: a node’s kernel panics, a VM is lost, a zone goes down, the kubelet evicts a Pod because the node ran out of memory. A PDB can’t prevent any of these; the Pod is already gone.

Voluntary disruptions are actions someone or something initiated: kubectl drain, a cluster autoscaler removing an underused node, a managed upgrade rolling the node pool, a descheduler rebalancing. These go through the Eviction API, a subresource (pods/eviction) that checks every PDB matching the Pod before allowing the delete. If the eviction would violate a budget, the API server answers 429 Too Many Requests and the caller is expected to wait and retry.

The important detail: only callers that use the Eviction API are bound by budgets. kubectl delete pod skips it. So does scaling a Deployment down, and so does a Deployment’s own rolling update (which is governed by maxUnavailable on the Deployment, not the PDB). A NoExecute taint evicts without consulting PDBs too. Interviewers like asking “does a PDB stop a rollout from taking Pods down?” The answer is no.

Writing a budget

A PDB has a label selector and one of two fields:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: checkout
  namespace: shop
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      app: checkout
yaml

maxUnavailable: 1 means “at most one of the selected Pods may be unavailable because of an eviction at any moment”. The alternative is minAvailable, which states the floor instead. Both accept an integer or a percentage. You can’t set both.

The controller keeps a running calculation in the PDB’s status. With a checkout Deployment of 4 replicas, all healthy:

$ kubectl get pdb -n shop checkout
NAME       MIN AVAILABLE   MAX UNAVAILABLE   ALLOWED DISRUPTIONS   AGE
checkout   N/A             1                 1                     2d
Terminal

ALLOWED DISRUPTIONS is what matters. The status fields behind it are expectedPods (4, read from the owning Deployment’s replica count), currentHealthy (Pods that are Ready), and desiredHealthy (4 minus 1, so 3). Allowed disruptions is currentHealthy - desiredHealthy, never below zero.

Walk through a drain of a node holding two checkout Pods. The drain asks to evict the first: allowed is 1, so it succeeds and allowed drops to 0. It asks for the second and gets 429. It retries every few seconds. Meanwhile the Deployment notices it has 3 Pods, creates a replacement on another node, and once that Pod becomes Ready, currentHealthy is back to 4 and allowed returns to 1. The second eviction now succeeds. The drain finishes, having never let checkout drop below 3 Ready Pods.

That walkthrough shows why the budget relies on readiness. If the new Pod has no readiness probe, it counts as healthy the instant its container starts, and the budget protects nothing real. See the probes guide for getting that signal right.

Choosing minAvailable or maxUnavailable

Percentages are converted to Pod counts by rounding up, in both fields. minAvailable: 50% of 5 Pods becomes 3 that must stay, which is cautious. maxUnavailable: 50% of 5 also becomes 3, but here rounding up means more Pods may go at once, which is the less cautious direction. Integers avoid having to remember that.

The bigger difference is how each behaves when the replica count changes. minAvailable: 3 was a sensible budget for 4 replicas. Scale to 3 replicas and it allows zero evictions. Scale to 20 and it allows 17 at once, which isn’t what anyone meant. maxUnavailable: 1 (or a percentage) keeps the same meaning at any size, which is why it’s usually the better default for Deployments. minAvailable makes sense when the requirement really is an absolute number, such as a quorum: a 5-member etcd cluster needs 3 members, so minAvailable: 3.

Also, maxUnavailable and percentage minAvailable only work when the controller can find the expected replica count, which means the Pods must belong to a Deployment, ReplicaSet, StatefulSet or ReplicationController (or a custom resource that implements the scale subresource). For bare Pods, only an integer minAvailable works.

Budgets that block maintenance

The failure mode platform teams complain about is a PDB that allows zero disruptions forever. Three common causes:

  • A budget equal to the replica count. minAvailable: 1 on a single-replica Deployment, or maxUnavailable: 0. The drain never progresses. kubectl prints the reason on each retry:
evicting pod shop/legacy-reports-7c9d8b6f5-qx2lm
error when evicting pods/"legacy-reports-7c9d8b6f5-qx2lm" -n "shop" (will retry after 5s):
Cannot evict pod as it would violate the pod's disruption budget.
Text
  • Pods that are already unhealthy. If 2 of 4 Pods are in CrashLoopBackOff, currentHealthy is 2 against a desired 3, so allowed disruptions is 0. The broken Pods can’t be evicted either, by default, even though evicting them costs nothing.
  • Overlapping PDBs. If two budgets select the same Pod, the Eviction API refuses the eviction with an error, because it can’t decide which budget applies.

Managed platforms handle the first case in different ways. Some honour the budget for a fixed window (an hour, for instance) and then force the node down anyway. So a zero-disruption budget doesn’t keep a Pod alive through an upgrade; it just turns a polite eviction into a forced one later.

The second case has a direct fix. unhealthyPodEvictionPolicy, stable since 1.31 (beta and on by default in 1.27 to 1.30), lets the Eviction API evict Pods that aren’t Ready regardless of the budget:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: checkout
  namespace: shop
spec:
  maxUnavailable: 1
  unhealthyPodEvictionPolicy: AlwaysAllow
  selector:
    matchLabels:
      app: checkout
yaml

With AlwaysAllow, a crashlooping Pod no longer holds the drain hostage; the budget still protects the healthy ones. The default, IfHealthyBudget, only allows evicting unhealthy Pods when the application isn’t already below its budget.

For a genuinely single-replica workload, the honest answer is that a PDB can’t give it zero-downtime maintenance. Either run two replicas or accept the disruption and schedule maintenance for a quiet window.

Interview practice

“Your platform team reports that node upgrades stall on a node running search-indexer, which has 3 replicas and a PDB with minAvailable: 3. The service owner says the budget is there because the indexer can’t lose capacity. What do you recommend?”

Answer

With 3 replicas and minAvailable: 3, desiredHealthy equals the replica count, so allowed disruptions is permanently 0. Every eviction returns 429 and the drain retries forever (or until the platform force-drains, which is worse for the owner because it removes the Pod without waiting). The budget isn’t protecting capacity; it’s preventing the polite path.

I’d ask what capacity the indexer actually needs. If 3 Ready Pods is truly the minimum, then the Deployment should run 4 replicas with maxUnavailable: 1, so one can always move while 3 serve. That’s the usual pattern: the budget expresses tolerated loss, and the replica count provides the headroom. I’d also set unhealthyPodEvictionPolicy: AlwaysAllow so a crashing Pod can’t block future drains, check that the Pods have a real readiness probe so “healthy” means something, and confirm the Deployment’s rollout settings (maxSurge/maxUnavailable) match the same capacity requirement, since the PDB doesn’t govern rollouts.

Further reading

More in Kubernetes

esc