calricon.

General

The Kubernetes Pod Disruption Budget Deadlock: Why Restrictive PDBs Paralyze Cluster Upgrades and Node Drains

Oct 24, 2026/9 min read

calricon / field note

PodDisruptionBudgets (PDBs) are supposed to protect application uptime during voluntary maintenance, yet misconfigured budgets routinely trigger catastrophic cluster deadlocks. When developers set 'minAvailable: 100%' or a crashing pod violates the budget, node drains hang indefinitely—paralyzing security patches, AMI upgrades, and Karpenter autoscaling across the entire cluster. Here is how to engineer disruption-safe Kubernetes infrastructure.

Oct 24, 2026/General
242
Advertisement

The Kubernetes Pod Disruption Budget Deadlock: Why Restrictive PDBs Paralyze Cluster Upgrades and Node Drains

In enterprise cloud infrastructure, the PodDisruptionBudget (PDB) is treated as an indispensable reliability shield. Platform engineering teams mandate PDBs across every service to guarantee that voluntary disruptions—such as automated node drains, Amazon EKS AMI rolling upgrades, Karpenter node consolidation, and Spot instance terminations—never take down more replicas than an application can safely afford to lose. On a reliability scorecard, enforcing a PDB on every workload looks like world-class operational governance. In real-world cluster management, however, misconfigured disruption budgets trigger the Kubernetes Pod Disruption Budget Deadlock: developers write mathematical impossibilities like minAvailable: 100% or minAvailable: 1 on a single-replica pod, or an unrelated application crash consumes the failure budget, causing the Kubernetes Eviction API to throw permanent HTTP 429 Cannot evict pod errors. The resulting node drain freezes indefinitely, paralyzing cluster-wide security patching, locking autoscalers into expensive zombie states, and forcing on-call platform engineers into emergency manual kubectl delete pod overrides at 2:00 AM.

A PodDisruptionBudget is meant to be a safety floor, not an immovable concrete ceiling. When you configure minAvailable: 100% or attach a PDB to a single-replica deployment, you aren't protecting high availability—you are building a dead-man's switch that permanently bricks your cluster's automated node upgrades and autoscaling pipelines.

The Mechanics of the Eviction API and the PDB Controller

To understand how a single badly configured manifest in an isolated namespace can paralyze an entire multi-tenant Kubernetes cluster, platform architects must trace how node draining interacts with the Kubernetes Eviction subresource:

  • The Eviction API Handshake: When an administrative operator, Karpenter, or Cluster Autoscaler drains a worker node (kubectl drain <node>), it does not immediately terminate pods with the Core API. Instead, it creates an Eviction object (POST /api/v1/namespaces/{ns}/pods/{name}/eviction).
  • The Disruption Budget Check: The Kubernetes API server evaluates the pod against any matching PodDisruptionBudget. The PDB controller inspects the current number of healthy, running pods against the policy's spec.minAvailable or spec.maxUnavailable. If evicting the target pod would cause the number of healthy pods to drop below the threshold, the API server rejects the request with HTTP 429 Too Many Requests (StatusReason: Cannot evict pod as it would violate the pod's disruption budget).
  • The Infinite Drain Retry Loop: Draining tools do not give up on a 429 error; they retry the eviction periodically while holding the node in SchedulingDisabled (cordoned) state. As long as the PDB denies eviction, the drain command blocks forever, halting automated node pools, CI/CD cluster rotation pipelines, and infrastructure upgrade operators.

The Three Production Disasters of Misconfigured PDBs

When development teams copy-paste boilerplate PDB templates without understanding cluster-level eviction physics, three catastrophic operational failure modes emerge:

1. The "Zero-Tolerance" Deadlock (minAvailable: 100% or minAvailable == replicas)

Consider a critical microservice running with replicas: 3. A well-meaning engineer wants zero downtime during maintenance, so they write:

spec:
  minAvailable: 3 # or minAvailable: "100%"

Here is the mathematical trap: by demanding that all 3 replicas remain available at all times, the application can tolerate exactly 0 voluntary disruptions.

When Karpenter or an AMI upgrade operator attempts to drain a node hosting Pod #1, Kubernetes checks the math: 3 currently available minus 1 to be evicted equals 2, which is less than minAvailable: 3. The eviction is rejected. Because the pod cannot be evicted to another node, the drain hangs indefinitely. Even though the cluster has dozens of empty nodes ready to host the pod, the node rotation pipeline is permanently paralyzed.

2. The Single-Replica Zombie Pod (minAvailable: 1 on replicas: 1)

In internal tooling, legacy monoliths, or staging environments, services frequently run as singletons (replicas: 1). If a developer or a blanket Helm chart automatically generates a PDB with minAvailable: 1:

  • The pod is the sole instance of the service.
  • Evicting it would reduce the count from 1 to 0, which violates minAvailable: 1.
  • The pod can literally never be voluntarily evicted by any automated process.

When AWS issues a 2-minute Spot termination notice or a hardware retirement alert for that EC2 instance, the automated draining controller gets stonewalled by the PDB until the hard timeout expires, causing an ungraceful, sudden kill that corrupts state—the exact failure the PDB was intended to prevent.

3. The CrashLoop Cascade (Unhealthy Pods Blocking Healthy Evictions)

In older Kubernetes versions (and by default prior to Kubernetes 1.31), PDB evaluation suffered from a notorious flaw: unhealthy pods consumed your disruption allowance.

Suppose a payment service runs 5 replicas with minAvailable: 4 (allowing 1 pod down for maintenance). Now imagine a bad configuration change or database timeout causes Pod #5 to enter CrashLoopBackOff. The number of healthy pods drops from 5 to 4. The PDB is now at its absolute minimum allowed threshold (4 available).

If Karpenter now tries to consolidate an underutilized node that happens to host healthy Pod #1:

  • Evicting Pod #1 would drop healthy replicas to 3, so Kubernetes rejects the eviction.
  • Even worse: if an operator tries to drain the node hosting the broken Pod #5 to replace the server, Kubernetes blocked that too because the service was already below its healthy budget!

A single crashing pod in an application freezes node maintenance across the entire Kubernetes cluster, trapping worker nodes in unupgradable limbo.

Comparing Pod Disruption Budget Governance Models

Governance Dimension Naive minAvailable: 100% Default PDB (Unhealthy Blocked) Calricon Resilient PDB Architecture
Node Drain & Upgrade Velocity Permanently Blocked (100% deadlock) Unstable (Crashed pods paralyze drains) 100% Automated (Zero-hang drains; PDBs pass)
Application Uptime Protection Fragile (Forces hard kill on timeout) Moderate (Guarantees floor until bad deploy) High (maxUnavailable: 1 guarantees headroom)
Karpenter Consolidation Efficiency Broken (Cannot terminate idle EC2 nodes) Degraded (Stalls during partial failures) Optimal (Continuous bin-packing & shutdown)

The Calricon Production Playbook: Engineering Resilient Disruption Budgets

Eliminating PDB deadlocks while preserving strict application availability requires migrating from minAvailable to maxUnavailable, adopting unhealthyPodEvictionPolicy, and enforcing cluster-wide admission validation.

Step 1: Standardize on maxUnavailable: 1 (Never Set minAvailable == replicas)

Always specify disruption budgets using maxUnavailable rather than minAvailable. This guarantees that your budget dynamically scales with your replica count and explicitly communicates how many pods the cluster is allowed to evict simultaneously during maintenance:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: payment-service-pdb
  namespace: production
spec:
  # CRITICAL: Allows exactly 1 pod to be disrupted while guaranteeing replicas - 1 remain healthy
  maxUnavailable: 1
  # MANDATORY (K8s 1.27+, GA in 1.31): Broken pods do NOT block node drains
  unhealthyPodEvictionPolicy: AlwaysAllow
  selector:
    matchLabels:
      app: payment-service

For high-replica fleets (>10 pods), use a percentage ceiling (maxUnavailable: "20%") to allow parallel node draining without starving the service.

Step 2: Enforce unhealthyPodEvictionPolicy: AlwaysAllow Across All PDBs

Promoted to GA and enabled by default in Kubernetes 1.31 (and available in 1.27+), unhealthyPodEvictionPolicy solves the CrashLoop cascade:

  • IfHealthyBudget (Legacy default): A pod that is running but not ready can only be evicted if the application is currently at or above its healthy target. A single crashed pod deadlocks node drains.
  • AlwaysAllow (Calricon Standard): Running-but-unready pods (e.g., stuck in CrashLoopBackOff or failing readiness probes) are evicted immediately regardless of budget status, clearing dead weight off nodes so draining can proceed, while healthy pods remain strictly protected.

Step 3: Never Attach Blocking PDBs to Single-Replica Deployments

If an application cannot run with at least 2 replicas, it has no redundancy by definition. Setting a PDB on a single-replica deployment creates an illusion of high availability that only results in operational gridlock. Configure your deployment manifests so that any workload with replicas: 1 either omits a PDB entirely or sets maxUnavailable: 1. If an application must run as a singleton and cannot tolerate restarts, it must be scheduled on dedicated, non-disruptible node groups isolated from automated Karpenter consolidation.

Step 4: Deploy Kyverno Admission Rules to Reject Deadlock-Inducing PDBs

Do not rely on developer discipline to prevent cluster deadlocks. Enforce a Kyverno ClusterPolicy that validates PDBs at admission time, automatically blocking any PDB that sets minAvailable: 100% or specifies a minAvailable greater than or equal to the deployment's replica count:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: prevent-pdb-deadlocks
spec:
  validationFailureAction: Enforce
  rules:
    - name: reject-zero-disruption-pdbs
      match:
        any:
          - resources:
              kinds:
                - PodDisruptionBudget
      validate:
        message: "PDB deadlock detected: minAvailable cannot be '100%' and maxUnavailable cannot be 0. Use maxUnavailable: 1."
        pattern:
          spec:
            =(minAvailable): "!100%"
            =(maxUnavailable): "!0"

Step 5: Configure Drain Timeouts in Node Maintenance Scripts

Ensure that your automated cluster upgrade pipelines, Karpenter NodePool disruption budgets, and administrative scripts never hang indefinitely on a stuck PDB. Always pass explicit timeout and deletion flags to kubectl drain:

kubectl drain <node-name> \
  --ignore-daemonsets \
  --delete-emptydir-data \
  --timeout=180s \
  --grace-period=60

If an unevictable pod fails to clear after 180 seconds, the drain script alerts on-call SREs with the exact blocked pod and namespace instead of silently freezing node pool rotations.

Stop Letting Application Guardrails Paralyze Cluster Infrastructure

A PodDisruptionBudget is a contract between application developers and platform engineers: the platform team agrees not to disrupt your application faster than your budget allows, and you agree to provide an eviction path so the platform team can patch the underlying operating system and keep cloud costs lean.

Audit your Kubernetes PodDisruptionBudgets today. Ban minAvailable: 100% and maxUnavailable: 0 across all namespaces, enable unhealthyPodEvictionPolicy: AlwaysAllow on every PDB, strip disruption budgets off singletons, and enforce Kyverno validation at the admission gate. High availability is achieved by building applications that tolerate node turnover, not by holding the underlying cluster hostage.

Advertisement
Talk to Calricon