A PodDisruptionBudget is a floor, not a ceiling: it names how many replicas of a workload must stay available while something voluntarily takes the rest away. The thing doing the taking is usually kubectl drain, a node autoscaler, or an operator compacting nodes.
The distinction it encodes is between a crash and a decision. A node failing is an involuntary disruption and no budget can stop it; a drain is voluntary and the Eviction API refuses it once the budget is spent. Confusing the two produces the two classic outcomes: a budget that blocks maintenance forever, or a maintenance that takes quorum down. A budget is a statement about a controlled operation, and it is only as good as the health of the replicas behind it.
Only voluntary disruptions are gated
Voluntary disruptions are the ones someone chose: draining a node for repair, scaling a node group down, removing a pod to make room for a bigger one. Involuntary disruptions — hardware failure, a kernel panic, a node lost to a network partition, the kubelet evicting pods under memory pressure — are not gated by a budget at all. The docs are explicit that they still count against it, which is how a crash can silently consume the room your next drain needed.
The gate is the Eviction API, and it is the only gate. Deleting a pod or a Deployment directly bypasses every budget in the namespace, and so do rolling updates. expectedPods comes from the .spec.replicas of the workload that owns the pods, discovered through their ownerReferences, rather than from a count of matching pods — which is why a selector that matches pods from two controllers behaves strangely. A budget also cannot see pods that are already unhealthy: the default IfHealthyBudget policy refuses to evict a running-but-not-Ready pod while the application is disrupted, so a CrashLoopBackOff pod can pin a drain in place. AlwaysAllow exists precisely to break that deadlock.
The arithmetic is one subtraction
minAvailable sets desiredHealthy directly; maxUnavailable sets it to expectedPods minus the allowance. Then disruptionsAllowed is currentHealthy minus desiredHealthy, floored at zero. With three healthy replicas and minAvailable: 2, one eviction is allowed. Change it to minAvailable: 3 and none ever is: a valid manifest that permanently forbids maintenance. Two numbers in the status tell you which case you are in: expectedPods is the denominator and currentHealthy is the numerator, so a wrong denominator is a selector bug rather than a budget bug.
Percentages round up in both directions. minAvailable: "50%" on seven replicas leaves four healthy; maxUnavailable: "30%" on a single replica still allows that replica to go, for 100% unavailability. Only one of the two fields may be set, and the docs recommend maxUnavailable because it tracks replica-count changes: minAvailable: 3 is right at five replicas and wrong at two.
Quorum is a number you can compute
A three-member quorum service tolerates exactly one loss, so minAvailable: 2 or maxUnavailable: 1 encodes that and nothing more. maxUnavailable: 1 keeps working as the cluster grows; minAvailable: 3 stops making sense the moment the StatefulSet scales. A budget of one also means a drain can only ever move one replica at a time, and each move waits for the replacement to become Ready before the next eviction is admitted.
Rolling updates are the awkward neighbour. Pods unavailable during a rolling upgrade count against the budget, but Deployments and StatefulSets are not limited by budgets while rolling. A rolling update can therefore spend the whole allowance and leave a subsequent drain stalled, with the relationship between the two events visible nowhere except the PDB status.
When it breaks
minAvailable equal to the replica count. kubectl get pdb shows ALLOWED DISRUPTIONS 0 forever, every drain of a node holding those pods hangs, and the numbers look right so nobody suspects the budget. It is the single most common PDB misconfiguration, and it is indistinguishable from a healthy one until you try to do maintenance. The tell is that it never moves: a healthy budget’s ALLOWED count changes as pods restart, while a permanent 0 is a budget with no room.
The budget that guards nothing. A selector with a typo, or one matching labels no pod carries, yields expectedPods: 0 and disruptionsAllowed: 0 — it protects nothing and blocks nothing, and you find out when a drain succeeds and takes the workload below quorum. Check that expectedPods equals the replica count you believe you have. The failure is asymmetric: a too-strict budget blocks maintenance loudly, while a budget that matches nothing fails silently at the worst possible moment.
Check it quickly
Compare each budget against the workload it claims to guard:
kubectl get pdb -A -o custom-columns=NAME:.metadata.name,MIN:.spec.minAvailable,MAX:.spec.maxUnavailable,EXPECTED:.status.expectedPods,HEALTHY:.status.currentHealthy,ALLOWED:.status.disruptionsAllowed
kubectl get pdb zk-pdb -o jsonpath='{.spec.selector.matchLabels}{"\n"}'
kubectl get statefulset zookeeper -o jsonpath='{.spec.replicas}{"\n"}'
kubectl get pods -l app=zookeeper --field-selector=status.phase=Running --no-headers | wc -l
EXPECTED must equal the controller’s replica count; if it is 0 the selector matches nothing and the budget is decoration. ALLOWED at 0 while HEALTHY equals EXPECTED means the budget forbids every eviction, and ALLOWED at 0 while HEALTHY is lower means a crash already spent it.
Sources
- Configure a PodDisruptionBudget, minAvailable, maxUnavailable, percentage rounding and the zero-eviction case, checked 2026-09-16.
- Pod disruptions, voluntary versus involuntary disruption, eviction retries and rolling-update behaviour, checked 2026-09-16.