StackPractices
intermediate By Mathias Paulenko

Kubernetes Pod Disruption Budget Template

A template for defining Pod Disruption Budgets to control voluntary disruptions during node drains, upgrades, and maintenance windows.

Topics: devops

Overview

A Pod Disruption Budget (PDB) tells Kubernetes how much of your workload can be voluntarily disrupted at once. When someone runs kubectl drain to take a node down for an upgrade, the eviction API checks every PDB that covers a pod before removing it. Without a PDB, nothing stops a drain from evicting every replica of a service in a few seconds — and your load balancer notices before your pods reschedule somewhere else.

PDBs only govern voluntary disruptions: drains, rolling maintenance, cluster autoscaler scale-downs, and manual evictions through the API. They do nothing for involuntary ones — a kernel panic, a dead disk, or a network partition kills pods regardless of what your budget says. Replica controllers handle those cases by scheduling replacements; the PDB’s job is narrower: rate-limit how fast an operator (or the autoscaler) can tear down healthy pods on purpose.

That distinction matters when you write the budget. A PDB isn’t an availability guarantee — it’s a throttle. If a node fails the same minute you start a drain, you can still drop below your minimum because voluntary and involuntary disruptions are counted together. Size your replicas with headroom for that overlap, and treat the PDB as protection against maintenance mistakes, not against physics.

This template gives you a complete, copy-paste-ready PDB document for a production service: strategy selection, per-service examples, drain interaction, Helm integration, monitoring, and troubleshooting. The ready-to-apply manifests live in the companion repo. Pair it with the Helm Chart Review Checklist when you ship PDBs inside charts, and with Kubernetes Resource Quotas for the capacity side of multi-tenant clusters.

When to Use

  • Multi-replica Deployments or StatefulSets that can’t afford losing all pods at once during a drain
  • Cluster upgrade workflows where nodes are drained on a schedule
  • Managed Kubernetes (EKS, GKE, AKS) where the provider rotates nodes during control-plane or node-pool updates
  • Services with compliance or SLO requirements that document minimum availability during maintenance
  • Teams using the cluster autoscaler or Karpenter, where scale-down drains nodes without a human watching

Skip PDBs for single-replica workloads that tolerate downtime (a PDB with minAvailable: 1 just blocks every drain), for DaemonSets (one pod per node, eviction doesn’t apply the same way), and for Jobs or CronJobs whose pods are ephemeral by design.

Solution

# Pod Disruption Budget — `<Service Name>`

## PDB Overview

| Field | Value |
|-------|-------|
| Service | example-app |
| Namespace | production |
| Deployment | example-app |
| Min Replicas | 3 |
| PDB Strategy | minAvailable |
| Min Available | 2 |
| Max Unavailable | — |
| API Version | policy/v1 |
| Last Updated | 2026-10-19 |
| Owner | DevOps Team |

## 1. PDB Definitions

### Standard PDB (minAvailable)

```yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: example-app-pdb
  namespace: production
  labels:
    app.kubernetes.io/name: example-app
    app.kubernetes.io/instance: example-app
    app.kubernetes.io/managed-by: helm
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app.kubernetes.io/name: example-app
      app.kubernetes.io/instance: example-app
```

### PDB with maxUnavailable

```yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: example-app-pdb
  namespace: production
  labels:
    app.kubernetes.io/name: example-app
    app.kubernetes.io/instance: example-app
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: example-app
      app.kubernetes.io/instance: example-app
```

### PDB with Percentage

```yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: example-app-pdb
  namespace: production
spec:
  minAvailable: 50%
  selector:
    matchLabels:
      app.kubernetes.io/name: example-app
```

## 2. PDB Strategy Selection

| Strategy | Value | When to Use | Example |
|----------|-------|-------------|---------|
| minAvailable (absolute) | 2 | Fixed minimum replicas needed | 3 replicas, always need 2 |
| minAvailable (percentage) | 50% | Scales with replica count | 10 replicas, need 5 |
| maxUnavailable (absolute) | 1 | Control disruption rate | 5 replicas, allow 1 down |
| maxUnavailable (percentage) | 25% | Proportional disruption | 8 replicas, allow 2 down |

### Decision Matrix

| Replica Count | Recommended Strategy | Rationale |
|---------------|---------------------|-----------|
| 1 | No PDB (or minAvailable: 1) | Single replica, PDB blocks all drains |
| 2 | maxUnavailable: 1 | Allow 1 down, keep 1 running |
| 3 | minAvailable: 2 | Keep 2 running, allow 1 disruption |
| 5 | maxUnavailable: 1 | Allow 1 disruption at a time |
| 10+ | maxUnavailable: 25% | Proportional disruption control |

## 3. Service-Specific PDBs

### Web Frontend

| Field | Value |
|-------|-------|
| Deployment | web-frontend |
| Replicas | 6 |
| Strategy | maxUnavailable: 1 |
| Rationale | High traffic, can tolerate 1 pod down |
| HPA | Yes (min: 4, max: 12) |

```yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: web-frontend-pdb
  namespace: production
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: web-frontend
```

### API Service

| Field | Value |
|-------|-------|
| Deployment | api-service |
| Replicas | 4 |
| Strategy | minAvailable: 3 |
| Rationale | Critical path, need 75% available |
| HPA | Yes (min: 3, max: 8) |

```yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-service-pdb
  namespace: production
spec:
  minAvailable: 3
  selector:
    matchLabels:
      app.kubernetes.io/name: api-service
```

### Worker (Background Jobs)

| Field | Value |
|-------|-------|
| Deployment | background-worker |
| Replicas | 3 |
| Strategy | maxUnavailable: 1 |
| Rationale | Jobs are resumable, tolerate 1 down |
| HPA | No |

```yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: background-worker-pdb
  namespace: production
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: background-worker
```

### Database (StatefulSet)

| Field | Value |
|-------|-------|
| StatefulSet | postgres-cluster |
| Replicas | 3 |
| Strategy | minAvailable: 2 |
| Rationale | Quorum-based, need majority |
| HPA | No |

```yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: postgres-pdb
  namespace: production
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app.kubernetes.io/name: postgres-cluster
```

### Redis Cache

| Field | Value |
|-------|-------|
| Deployment | redis-cache |
| Replicas | 1 |
| Strategy | No PDB |
| Rationale | Single replica, PDB would block all drains |

### Scheduled Job (CronJob)

| Field | Value |
|-------|-------|
| CronJob | data-export |
| Strategy | No PDB |
| Rationale | Jobs are ephemeral, rescheduled by CronJob controller |

## 4. PDB and Node Drain Interaction

### How PDB Affects Node Drains

| Scenario | Pod Count | PDB | Drain Behavior |
|----------|-----------|-----|----------------|
| 3 pods, minAvailable: 2 | 3 | 2 | Evict 1 at a time, wait for new pod before next |
| 3 pods, minAvailable: 3 | 3 | 3 | Drain blocked — cannot evict any pod |
| 5 pods, maxUnavailable: 1 | 5 | 1 | Evict 1 at a time |
| 2 pods, maxUnavailable: 1 | 2 | 1 | Evict 1, wait for replacement |
| 1 pod, minAvailable: 1 | 1 | 1 | Drain blocked — cannot evict the only pod |

### Drain Commands

```bash
kubectl drain node-1 --ignore-daemonsets --delete-emptydir-data

## Drain with timeout (PDB may block)
kubectl drain node-1 --ignore-daemonsets --delete-emptydir-data --timeout=5m

## Force drain (bypasses PDB — use with caution)
kubectl drain node-1 --ignore-daemonsets --delete-emptydir-data --force

## Check drain status
kubectl get pods -o wide --field-selector spec.nodeName=node-1

## Uncordon node after maintenance
kubectl uncordon node-1
```

### PDB Status Check

```bash
## Check PDB status
kubectl get pdb -n production

## Detailed PDB status
kubectl describe pdb example-app-pdb -n production

## Check which pods are protected
kubectl get pods -n production -l app.kubernetes.io/name=example-app
```

## 5. PDB in Helm Charts

### values.yaml

```yaml
podDisruptionBudget:
  enabled: true
  minAvailable: 2
  # maxUnavailable: 1  # Alternative to minAvailable
```

### Template

```yaml
{{- if .Values.podDisruptionBudget.enabled }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: {{ include "chart.fullname" . }}
  labels:
    {{- include "chart.labels" . | nindent 4 }}
spec:
  {{- if .Values.podDisruptionBudget.minAvailable }}
  minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
  {{- end }}
  {{- if .Values.podDisruptionBudget.maxUnavailable }}
  maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }}
  {{- end }}
  selector:
    matchLabels:
      {{- include "chart.selectorLabels" . | nindent 6 }}
{{- end }}
```

## 6. Monitoring PDBs

### Key Metrics

| Metric | Description | Alert Threshold |
|--------|-------------|-----------------|
| PDB allowed disruptions | Number of pods that can be disrupted | < 0 (blocked) |
| PDB current healthy | Currently healthy pods | < minAvailable |
| PDB desired healthy | Target healthy pods | — |
| PDB expected pods | Total pods matching selector | — |
| Drain blocked duration | Time a drain is blocked by PDB | > 10 min |

### Alerting Rules

```yaml
## Prometheus alerting rules
groups:
  - name: pdb-alerts
    rules:
      - alert: PDBBlocked
        expr: kube_poddisruptionstatus_allowed_disruptions == 0
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "PDB {{ $labels.pdb }} blocks all disruptions"
          description: "No pods can be disrupted for {{ $labels.namespace }}/{{ $labels.pdb }}"

      - alert: PDBBelowMinAvailable
        expr: kube_poddisruptionstatus_current_healthy < kube_poddisruptionstatus_desired_healthy
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "PDB {{ $labels.pdb }} below min available"
          description: "Only {{ $value }} healthy pods, need {{ $labels.desired_healthy }}"
```

## 7. PDB Troubleshooting

### Common Issues

| Issue | Cause | Resolution |
|-------|-------|------------|
| Drain stuck | PDB blocks eviction (minAvailable = replicas) | Scale up deployment, then drain |
| Drain stuck | Not enough replicas to satisfy PDB | Scale up before draining |
| PDB not found | PDB not created or wrong namespace | Verify PDB exists: `kubectl get pdb -n <ns>` |
| PDB selector mismatch | Selector doesn't match pod labels | Verify labels: `kubectl get pods --show-labels` |
| Eviction timeout | Pod takes too long to terminate | Add terminationGracePeriodSeconds |

### Debugging Steps

```bash
## 1. Check PDB status
kubectl get pdb -n production
kubectl describe pdb example-app-pdb -n production

## 2. Check pod count vs PDB requirements
kubectl get pods -n production -l app.kubernetes.io/name=example-app --no-headers | wc -l

## 3. Check if pods are healthy
kubectl get pods -n production -l app.kubernetes.io/name=example-app

## 4. Check node drain status
kubectl get nodes -o wide
kubectl describe node node-1 | grep -A 10 "Conditions:"

## 5. Check eviction events
kubectl get events -n production --field-selector reason=Eviction
```

How the Eviction Flow Works

When kubectl drain runs, it doesn’t delete pods — it sends evictions through the /eviction subresource of the pod API. That indirection is the whole point: a plain kubectl delete pod ignores budgets entirely, while an eviction gets checked against every PDB whose selector matches the pod. The eviction API answers with 429 Too Many Requests when the budget would be violated, and the drain retries until the pod’s replacement is ready on another node.

Mermaid flowchart LR diagram

Three details in that loop surprise people in production. First, the check counts current healthy pods, not desired replicas — if a pod is crash-looping, it already counts against your budget, so a drain can stall even with plenty of desired replicas. Second, eviction is per-pod and sequential: the drain won’t evict pod two until pod one’s replacement reports Ready, which is why a slow-starting service can turn a two-minute drain into twenty. Third, kubectl drain --force and kubectl delete pod both skip this path, which is why forcing a drain is the fastest way to violate your own availability floor.

Explanation

The two budget fields, minAvailable and maxUnavailable, are mutually exclusive — pick one per PDB. minAvailable sets a floor: “never go below N healthy pods.” maxUnavailable sets a ceiling on disruption rate: “never take down more than N at once.” Both accept absolute numbers or percentages, and percentages round up for minAvailable and down for maxUnavailable — a quirk that matters at small replica counts, where minAvailable: 50% on three pods requires two healthy.

For most services, maxUnavailable: 1 is the sanest default. It doesn’t care how many replicas you run, it survives HPA scale-ups without edits, and it still guarantees drains proceed one pod at a time. Reach for minAvailable when the floor itself is the requirement — a quorum-based StatefulSet like etcd or a database that needs a majority to keep serving writes.

Single-replica deployments are the classic trap. A PDB with minAvailable: 1 on one pod makes every drain hang forever, because evicting the only replica would violate the budget. Either scale to two or more replicas before enabling the PDB, or skip the PDB and accept that drains will cause a brief outage. A blocked drain also stalls whatever triggered it — including the cluster autoscaler, which will sit waiting instead of reclaiming an empty node.

The autoscaler interaction cuts both ways. When the autoscaler (or Karpenter) decides a node is underutilized, it drains the node first and that drain honors PDBs. If your PDB can’t be satisfied, scale-down aborts and you keep paying for the node. unhealthyPodEvictionPolicy: AlwaysAllow (policy/v1, stable since Kubernetes 1.31) softens this by letting eviction proceed for pods that aren’t Ready yet — useful for crash-looping workloads, dangerous for anything else.

Don’t confuse the PDB budget with the Deployment’s maxUnavailable in rollingUpdate. They look similar but live in different planes: the rolling-update field controls how many pods may be down during a deployment, and pods being replaced don’t count against the PDB because the Deployment temporarily runs extra replicas. A PDB protects you during maintenance; the rolling-update field protects you during deploys. You need both, tuned separately.

Variants

ContextApproachNotes
Single replicaNo PDB or minAvailable: 0Accept downtime during drains
StatefulSetminAvailable for quorumDatabases need majority
DaemonSetNo PDB neededDaemonSet pods run on every node
Job/CronJobNo PDB neededEphemeral pods, rescheduled
HPA-scaledUse percentage PDBScales with replica count
Multi-AZOne PDB per AZPrevents all-AZ disruption

One more variant worth knowing: minAvailable: 0 or maxUnavailable: 100% effectively disables the budget while keeping the PDB object around. Teams use this as a documented opt-out — the object exists, reviewers see the decision, and nobody mistakes the missing PDB for an oversight during an audit. If you adopt it, add a comment in the manifest explaining why the budget is intentionally permissive, or the next person will “fix” it for you.

What Works

  1. Always set a PDB on multi-replica deployments — it stops drains from evicting everything at once.
  2. Default to maxUnavailable: 1 unless a service genuinely needs a fixed floor.
  3. Prefer percentage budgets on HPA-managed workloads so the budget tracks replica count.
  4. Scale up before draining when the current count can’t satisfy the PDB.
  5. Alert on allowedDisruptions == 0 — a blocked PDB discovered during an incident is too late.
  6. Exercise PDBs in game days before you trust them in a real upgrade.
  7. Ship PDBs inside Helm charts so they’re impossible to forget per-service.

Common Mistakes

  1. PDB on a single-replica deployment — every drain hangs forever.
  2. minAvailable equal to the replica count — no pod can ever be evicted.
  3. Skipping PDBs on critical services — one drain takes the whole service down.
  4. A selector that doesn’t match pod labels — the PDB exists but protects nothing.
  5. No alerting on PDB status — blocked drains get discovered mid-incident.
  6. Forgetting the PDB when scaling down — the budget can stall the autoscaler.
  7. Using policy/v1beta1 — it’s deprecated and removed; use policy/v1.
  8. Expecting the PDB to cover node failures — it only governs voluntary evictions.

See Also

Further Reading

Frequently Asked Questions

What happens if a PDB blocks a node drain?

The drain waits — by default, indefinitely. Pass --timeout to bound the wait. If the budget can never be satisfied (fewer replicas than minAvailable), the drain blocks until timeout or until you scale up, relax the PDB, or use --force, which bypasses the budget and can cause downtime.

Should I use minAvailable or maxUnavailable?

Use minAvailable when you need a guaranteed floor — "always keep two pods running." Use maxUnavailable when you want to control the disruption rate — "never drop more than one at a time." For most services maxUnavailable: 1 is a good default because it survives replica-count changes without edits.

Do PDBs protect against hardware failures?

No. PDBs apply only to voluntary disruptions like drains and API evictions. A dead node is handled by the replica controller rescheduling pods elsewhere. If a node dies while a drain is also in progress, both count against your healthy pod count — size replicas with headroom for that overlap.

Can I have both minAvailable and maxUnavailable in one PDB?

No — they're mutually exclusive within a single PDB. Pick the one that matches how you think about availability: a floor you can't cross, or a rate you can't exceed.

How do PDBs interact with the cluster autoscaler?

The autoscaler drains a node before removing it, and that drain honors PDBs. A PDB that can't be satisfied stalls scale-down until the autoscaler's timeout — tune --max-graceful-termination-sec — and then the node survives. It's a feature: the autoscaler can't quietly kill your quorum. Set unhealthyPodEvictionPolicy: AlwaysAllow if Not-Ready pods are the reason your drains stall.

Why did kubectl delete pod work even though the PDB blocked my drain?

Because delete isn't an eviction. The PDB only guards the /eviction API path — drains and autoscaler evictions go through it, direct deletions don't. If you need to remove a pod during maintenance, evict it (kubectl drain, or POST to the eviction subresource) rather than deleting it.