Lesson 48 of 66 · 18 min read · Updated

PriorityClass, Preemption and PodDisruptionBudgets

The previous two articles were about where a Pod should run. This one is about two harder questions. What happens when there is no room left, and which Pod has to give way? And what happens when you deliberately take a node away for maintenance, and how do you stop that from taking your API down?

The answers are three objects that work closely together. A PriorityClass ranks Pods. Preemption lets the scheduler evict lower-ranked Pods to make room for a higher-ranked one. A PodDisruptionBudget limits how many Pods of an application voluntary operations such as kubectl drain may take away at the same time.

We will cover the following topics:

  1. What PriorityClass objects are, and the built-in system-cluster-critical and system-node-critical classes
  2. How preemption works, and how to see it happen without overloading a real cluster
  3. preemptionPolicy: Never for workloads that should jump the queue but never evict anyone
  4. PodDisruptionBudgets with minAvailable and maxUnavailable
  5. The difference between evicting a Pod and deleting it
  6. Running kubectl drain safely, and why it sometimes hangs
  7. unhealthyPodEvictionPolicy for budgets that block maintenance forever

Technical Requirements

  1. A multi-node cluster. The examples use Docker Desktop's Kubernetes with the kind provisioner (desktop-control-plane plus desktop-worker, desktop-worker2, desktop-worker3), Kubernetes v1.36. A plain kind cluster with three workers behaves the same with kind-* node names.
  2. kubectl with cluster-admin rights: PriorityClasses are cluster-scoped, and the preemption demo patches a node's status.
  3. The node-info-api:1.0 image from the first article in this section, or any small image that keeps running.

Core Concepts

PriorityClass

A PriorityClass is a cluster-scoped object that maps a name to an integer. Pods reference it by name in spec.priorityClassName, and the admission controller copies the integer into spec.priority.

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: lesson-online-high
value: 100000
globalDefault: false
description: "Customer-facing work. May preempt batch pods."
FieldMeaning
valueThe priority. Higher wins. User-defined classes must be at most 1,000,000,000.
globalDefaultIf true, Pods without a priorityClassName get this class. Only one class may be the global default.
preemptionPolicyPreemptLowerPriority (default) or Never.
descriptionFree text for humans. Use it; it is the only documentation most people will read.

Pods with no class and no global default get priority 0.

Every cluster ships with two classes reserved for system components:

ClassValueUsed by
system-cluster-critical2,000,000,000Cluster add-ons such as CoreDNS and metrics-server
system-node-critical2,000,001,000Per-node components such as kube-proxy and CNI agents

Do not give these to application Pods. They exist so that DNS and networking win every contest for resources, because nothing else works without them.

Priority has two effects. In the scheduling queue, Pods with higher priority are tried first. And when no node has room, a high-priority Pod may preempt lower-priority Pods.

How Preemption Works

When a Pod fails to schedule, the scheduler asks: "If I removed some lower-priority Pods from one node, would this Pod fit there?" If the answer is yes for at least one node, it:

  1. Picks the node where the fewest and lowest-priority victims are needed.
  2. Sets the pending Pod's status.nominatedNodeName to that node.
  3. Evicts the victims gracefully, honouring their terminationGracePeriodSeconds.
  4. Schedules the pending Pod once the room is free.

A few rules matter in practice:

  1. Only lower-priority Pods can be victims. Equal priority is never preempted.
  2. The scheduler tries to respect PodDisruptionBudgets of victims, but it may violate them if there is no other option. Preemption is not a "voluntary disruption" in the strict sense.
  3. Preemption does not help when the problem is not resources. A Pod that is Pending because of a node selector or a taint will see "Preemption is not helpful for scheduling" in its events, as you saw in the previous articles.
  4. The victims' controllers (Deployments, ReplicaSets) recreate them, and the new Pods go back into the queue with their original, lower priority.

preemptionPolicy: Never

Some workloads should go to the front of the queue but must never kill anyone, for example a data-science job that is important but not urgent. A class with preemptionPolicy: Never gives the Pod its high priority in the queue, so it is placed before lower-priority Pods when room appears, but the scheduler will not evict anything for it.

Voluntary and Involuntary Disruptions

Kubernetes separates two kinds of disruption:

InvoluntaryVoluntary
Node hardware failure, kernel panickubectl drain before node maintenance or upgrade
Cloud VM deleted by the providerCluster autoscaler removing a node
Node out of memory, kubelet evictionDeleting a Pod by hand, rolling out a new version
Network partitionMoving Pods with a descheduler

A PodDisruptionBudget can only protect against voluntary disruptions, and only those that go through the Eviction API. It cannot stop hardware from failing.

PodDisruptionBudget

A PDB selects Pods with a label selector and states how many must stay available, or how many may be unavailable, during voluntary disruptions:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: cart-api-pdb
spec:
maxUnavailable: 1
selector:
matchLabels:
app: cart-api
FieldMeaning
minAvailableAt least this many (or this percentage) of selected Pods must stay healthy.
maxUnavailableAt most this many (or this percentage) may be unhealthy at once. Usually the better choice, because it scales with the Deployment.
selectorWhich Pods the budget covers. Normally the same labels as the Deployment.
unhealthyPodEvictionPolicyWhat to do with Pods that are running but not Ready. IfHealthyBudget (default) or AlwaysAllow.

Set only one of minAvailable and maxUnavailable. "Healthy" means the Pod's Ready condition is true, so a good readiness probe is a prerequisite for a meaningful budget.

The controller keeps four numbers in the PDB's status: expectedPods, currentHealthy, desiredHealthy and disruptionsAllowed. The last one is what the Eviction API checks.

Eviction Versus Deletion

This is the most misunderstood part of PDBs.

OperationAPI callChecks PDB?Used by
EvictionPOST .../pods/<name>/evictionYeskubectl drain, cluster autoscaler, node upgrades
DeletionDELETE .../pods/<name>Nokubectl delete pod, controllers scaling down, rollouts

An eviction that would push the application below its budget is rejected with HTTP 429 Too Many Requests. The caller is expected to retry later. A deletion is never rejected by a PDB. So a PDB protects you from well-behaved automation, not from someone running kubectl delete pod by hand. Deployment rollouts use maxUnavailable/maxSurge on the Deployment for their own safety; they do not consult the PDB.

unhealthyPodEvictionPolicy

Imagine an API whose Pods are all running but none is Ready, perhaps because a dependency is down. A budget of minAvailable: 2 now has currentHealthy: 0. With the default policy, IfHealthyBudget, a not-Ready Pod can only be evicted if the application is currently meeting its budget. It is not, so nothing can be evicted, and kubectl drain hangs on that node indefinitely, even though evicting broken Pods would hurt no one.

Setting unhealthyPodEvictionPolicy: AlwaysAllow tells the Eviction API that Pods which are not Ready may always be evicted. Healthy Pods are still protected by the budget. For most stateless APIs this is the better setting; the Kubernetes documentation recommends it unless your application needs the stricter behaviour, for example a database that must not lose a not-yet-ready replica that is still catching up.

Hands-On: Kubernetes Commands

List PriorityClasses

Shows value, global default, and preemption policy.

kubectl get priorityclass

Show the priority each Pod received

spec.priority is the integer the admission controller filled in.

kubectl get pods -n sched-priority -o custom-columns=NAME:.metadata.name,PRIORITY:.spec.priority,CLASS:.spec.priorityClassName,STATUS:.status.phase

Find preemption events

The scheduler emits a Preempted event on every victim.

kubectl get events -n sched-priority --sort-by=.lastTimestamp | grep -i -E "preempt|FailedScheduling"

List PodDisruptionBudgets with allowed disruptions

ALLOWED DISRUPTIONS is the number that matters during maintenance.

kubectl get pdb -A

Read the full status of a budget

Includes currentHealthy, desiredHealthy and the DisruptionAllowed condition with a reason.

kubectl get pdb inventory-api-pdb -n sched-priority -o yaml

Drain only your own Pods from a node

--pod-selector limits eviction to matching Pods; the node is still cordoned. Always uncordon afterwards.

kubectl drain desktop-worker --pod-selector=app=cart-api --timeout=30s
kubectl uncordon desktop-worker

Call the Eviction API directly

Useful to see the raw 429 response a PDB produces.

kubectl create --raw /api/v1/namespaces/sched-priority/pods/<pod>/eviction -f eviction.json

Step-by-Step Example

The Scenario

Your shop has a few nodes with expensive accelerators used both by a nightly report generator and by a real-time fraud-scoring service. When accelerators are scarce, fraud scoring must win. Separately, the platform team needs to drain worker nodes for upgrades without ever taking the cart API below two healthy replicas.

Filling real CPU or memory to force preemption would disturb everything else on a shared cluster, so the example uses a fake extended resource instead. You will tell Kubernetes that desktop-worker3 has two units of dotnetacademy.dev/gpu. Only Pods that request this resource compete for it, so nothing else on the cluster is affected.

Step 1 — Look at the Built-in Classes

kubectl get priorityclass
NAME VALUE GLOBAL-DEFAULT AGE PREEMPTIONPOLICY
system-cluster-critical 2000000000 false 7d13h PreemptLowerPriority
system-node-critical 2000001000 false 7d13h PreemptLowerPriority

There is no global default, so every Pod in the cluster that does not ask for a class has priority 0.

Step 2 — Advertise a Fake Extended Resource

Extended resources are added to a node's status.capacity with a JSON patch on the status subresource. The / in the resource name is written as ~1 in JSON Pointer syntax.

kubectl patch node desktop-worker3 --subresource=status --type=json \
-p '[{"op":"add","path":"/status/capacity/dotnetacademy.dev~1gpu","value":"2"}]'

The kubelet copies it into allocatable on its next status update, usually within a few seconds:

kubectl describe node desktop-worker3 | grep -E "Capacity|Allocatable|gpu"
Capacity:
dotnetacademy.dev/gpu: 2
Allocatable:
dotnetacademy.dev/gpu: 2

This is the same mechanism a real device plugin uses to advertise GPUs; only the name is invented.

Step 3 — Create the Namespace and PriorityClasses

apiVersion: v1
kind: Namespace
metadata:
name: sched-priority
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: lesson-batch-low
value: 1000
globalDefault: false
description: "Nightly reports and other work that can wait."
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: lesson-online-high
value: 100000
globalDefault: false
description: "Customer-facing work. May preempt batch pods."
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: lesson-online-high-nonpreempting
value: 100000
globalDefault: false
preemptionPolicy: Never
description: "Jumps the queue but never evicts anyone."
kubectl apply -f 1-namespace.yaml -f 2-priorityclasses.yaml
kubectl get priorityclass
NAME VALUE GLOBAL-DEFAULT AGE PREEMPTIONPOLICY
lesson-batch-low 1000 false 0s PreemptLowerPriority
lesson-online-high 100000 false 0s PreemptLowerPriority
lesson-online-high-nonpreempting 100000 false 0s Never
system-cluster-critical 2000000000 false 7d13h PreemptLowerPriority
system-node-critical 2000001000 false 7d13h PreemptLowerPriority

Step 4 — Fill the Accelerators with Low-Priority Work

The report generator runs two replicas, each needing one accelerator. Extended resources must be requested as whole integers and, if a limit is given, it must equal the request.

apiVersion: apps/v1
kind: Deployment
metadata:
name: report-batch
namespace: sched-priority
spec:
replicas: 2
selector:
matchLabels:
app: report-batch
template:
metadata:
labels:
app: report-batch
spec:
priorityClassName: lesson-batch-low
containers:
- name: report-batch
image: node-info-api:1.0
imagePullPolicy: IfNotPresent
resources:
requests:
cpu: 50m
memory: 64Mi
dotnetacademy.dev/gpu: 1
limits:
dotnetacademy.dev/gpu: 1
kubectl apply -f 3-report-batch.yaml
kubectl get pods -n sched-priority -o custom-columns=NAME:.metadata.name,STATUS:.status.phase,NODE:.spec.nodeName
kubectl describe node desktop-worker3 | grep -A12 "Allocated resources" | grep -E "Resource|gpu"
NAME STATUS NODE
report-batch-d9695d8b9-bs9nk Running desktop-worker3
report-batch-d9695d8b9-xx2sv Running desktop-worker3

Resource Requests Limits
dotnetacademy.dev/gpu 2 2

Both replicas went to desktop-worker3, the only node with the resource, and they use both units. The accelerators are full.

Step 5 — A High-Priority Pod That May Not Preempt

Start with the polite version. It has the same high value but preemptionPolicy: Never:

apiVersion: v1
kind: Pod
metadata:
name: polite-scoring
namespace: sched-priority
labels:
app: polite-scoring
spec:
priorityClassName: lesson-online-high-nonpreempting
containers:
- name: polite-scoring
image: node-info-api:1.0
imagePullPolicy: IfNotPresent
resources:
requests:
cpu: 50m
memory: 64Mi
dotnetacademy.dev/gpu: 1
limits:
dotnetacademy.dev/gpu: 1
kubectl apply -f polite-scoring.yaml
kubectl describe pod polite-scoring -n sched-priority
Warning FailedScheduling 0s (x3 over 6s) default-scheduler 0/4 nodes are available: 1 node(s) had untolerated taint(s), 3 Insufficient dotnetacademy.dev/gpu. no new claims to deallocate, preemption: not eligible due to preemptionPolicy=Never.

It waits. The message says why in plain words: preemption was considered and refused because of the policy. Note also "3 Insufficient": the two workers without the resource count as having zero.

Step 6 — A High-Priority Pod That Preempts

Now the real fraud-scoring Pod, identical except for its name and the class lesson-online-high:

kubectl apply -f fraud-scoring.yaml
kubectl get pods -n sched-priority -o custom-columns=NAME:.metadata.name,STATUS:.status.phase,NODE:.spec.nodeName
NAME STATUS NODE
fraud-scoring Running desktop-worker3
polite-scoring Pending <none>
report-batch-d9695d8b9-bs9nk Running desktop-worker3
report-batch-d9695d8b9-fbn8n Pending <none>

One report Pod is gone and its replacement, fbn8n, is Pending. The events tell the story:

kubectl get events -n sched-priority --sort-by=.lastTimestamp | grep -i -E "preempt|Killing|FailedScheduling"
8s Warning FailedScheduling pod/fraud-scoring 0/4 nodes are available: 1 node(s) had untolerated taint(s), 3 Insufficient dotnetacademy.dev/gpu.
8s Normal Preempted pod/report-batch-d9695d8b9-xx2sv Preempted by pod 8fe7f45d-34e3-403d-9347-2f687cd88ffb on node desktop-worker3
8s Normal Killing pod/report-batch-d9695d8b9-xx2sv Stopping container report-batch
3s Warning FailedScheduling pod/report-batch-d9695d8b9-fbn8n 0/4 nodes are available: 1 node(s) had untolerated taint(s), 3 Insufficient dotnetacademy.dev/gpu. no new claims to deallocate, preemption: 0/4 nodes are available: 1 Insufficient dotnetacademy.dev/gpu, 3 Preemption is not helpful for scheduling.

The sequence is exactly the algorithm from Core Concepts: the fraud Pod failed to schedule, the scheduler picked a victim with lower priority, evicted it gracefully, and placed the fraud Pod in the freed slot. The recreated report Pod cannot preempt anything, because everything left on the accelerators has equal or higher priority.

Step 7 — Priority Also Orders the Queue

Two Pods are now waiting for an accelerator: polite-scoring (100000) and the recreated report Pod (1000). Free one slot:

kubectl delete pod fraud-scoring -n sched-priority
kubectl get pods -n sched-priority -o custom-columns=NAME:.metadata.name,PRIORITY:.spec.priority,CLASS:.spec.priorityClassName,STATUS:.status.phase
NAME PRIORITY CLASS STATUS
polite-scoring 100000 lesson-online-high-nonpreempting Running
report-batch-d9695d8b9-bs9nk 1000 lesson-batch-low Running
report-batch-d9695d8b9-fbn8n 1000 lesson-batch-low Pending

polite-scoring got the slot even though the report Pod had been waiting just as long. It never evicted anyone, yet its priority still put it first in line. That is precisely what preemptionPolicy: Never is for.

Clean up this part:

kubectl delete deploy report-batch -n sched-priority
kubectl delete pod polite-scoring -n sched-priority

Step 8 — A PodDisruptionBudget for the Cart API

To make the drain behaviour easy to see, pin all three cart API replicas to desktop-worker with a node selector. In production you would spread them, but a single node makes every eviction decision visible.

apiVersion: apps/v1
kind: Deployment
metadata:
name: cart-api
namespace: sched-priority
spec:
replicas: 3
selector:
matchLabels:
app: cart-api
template:
metadata:
labels:
app: cart-api
spec:
nodeSelector:
kubernetes.io/hostname: desktop-worker
containers:
- name: cart-api
image: node-info-api:1.0
imagePullPolicy: IfNotPresent
readinessProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 5
resources:
requests:
cpu: 50m
memory: 64Mi
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: cart-api-pdb
namespace: sched-priority
spec:
maxUnavailable: 1
selector:
matchLabels:
app: cart-api
kubectl apply -f 4-cart-api.yaml
kubectl get pdb -n sched-priority
NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGE
cart-api-pdb N/A 1 1 1s

Three healthy Pods, one may be disrupted.

Step 9 — Drain the Node

Drain desktop-worker, limited to the cart API Pods so nothing else on the node is touched, with a timeout so the command cannot hang forever:

kubectl drain desktop-worker --pod-selector=app=cart-api --timeout=30s
kubectl uncordon desktop-worker
node/desktop-worker cordoned
evicting pod sched-priority/cart-api-5dcb4b8fdd-ktdd7
evicting pod sched-priority/cart-api-5dcb4b8fdd-629lh
evicting pod sched-priority/cart-api-5dcb4b8fdd-jkg6h
error when evicting pods/"cart-api-5dcb4b8fdd-jkg6h" -n "sched-priority" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
error when evicting pods/"cart-api-5dcb4b8fdd-ktdd7" -n "sched-priority" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
pod/cart-api-5dcb4b8fdd-629lh evicted
evicting pod sched-priority/cart-api-5dcb4b8fdd-jkg6h
evicting pod sched-priority/cart-api-5dcb4b8fdd-ktdd7
error when evicting pods/"cart-api-5dcb4b8fdd-jkg6h" -n "sched-priority" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
...
node/desktop-worker uncordoned

Drain asked to evict all three at once. The Eviction API allowed exactly one, 629lh, and rejected the others. Drain retries every five seconds. In a real cluster the evicted Pod's replacement would start on another node, become Ready, disruptionsAllowed would go back to 1, and the next eviction would succeed. Here the replacement can only run on desktop-worker because of the node selector, and that node is cordoned, so the budget never recovers and drain gives up at the timeout.

kubectl get pods -n sched-priority -l app=cart-api
kubectl get pdb -n sched-priority
NAME READY STATUS RESTARTS AGE
cart-api-5dcb4b8fdd-jkg6h 1/1 Running 0 45s
cart-api-5dcb4b8fdd-ktdd7 1/1 Running 0 45s
cart-api-5dcb4b8fdd-mf6fq 0/1 ContainerCreating 0 2s

NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGE
cart-api-pdb N/A 1 0 27s

After uncordon, the replacement Pod starts and the budget will return to one allowed disruption once it is Ready. This is the everyday lesson of PDBs: a drain that "hangs" is usually a budget doing its job, and the fix is to make sure replacements can start somewhere else, not to delete the budget.

Never forget the uncordon. A cordoned node silently stops accepting new Pods.

kubectl delete deploy cart-api -n sched-priority
kubectl delete pdb cart-api-pdb -n sched-priority

Step 10 — When Unhealthy Pods Block Maintenance

Now an inventory API whose readiness probe points at an endpoint that does not exist, so its Pods run but never become Ready, protected by minAvailable: 2:

apiVersion: apps/v1
kind: Deployment
metadata:
name: inventory-api
namespace: sched-priority
spec:
replicas: 3
selector:
matchLabels:
app: inventory-api
template:
metadata:
labels:
app: inventory-api
spec:
nodeSelector:
kubernetes.io/hostname: desktop-worker
containers:
- name: inventory-api
image: node-info-api:1.0
imagePullPolicy: IfNotPresent
readinessProbe:
httpGet:
path: /ready # this endpoint does not exist, so the Pods never become Ready
port: 8080
periodSeconds: 5
resources:
requests:
cpu: 50m
memory: 64Mi
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: inventory-api-pdb
namespace: sched-priority
spec:
minAvailable: 2
selector:
matchLabels:
app: inventory-api
kubectl apply -f 5-inventory-api.yaml
kubectl get pods -n sched-priority -l app=inventory-api
kubectl get pdb inventory-api-pdb -n sched-priority
NAME READY STATUS RESTARTS AGE
inventory-api-95745979-h7crt 0/1 Running 0 25s
inventory-api-95745979-shrmn 0/1 Running 0 25s
inventory-api-95745979-v8w6s 0/1 Running 0 25s

NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGE
inventory-api-pdb 2 N/A 0 25s

Try to drain:

kubectl drain desktop-worker --pod-selector=app=inventory-api --timeout=20s
kubectl uncordon desktop-worker
node/desktop-worker cordoned
evicting pod sched-priority/inventory-api-95745979-shrmn
evicting pod sched-priority/inventory-api-95745979-v8w6s
...
There are pending nodes to be drained:
There are pending pods in node "desktop-worker" when an error occurred: [error when evicting pods/"inventory-api-95745979-shrmn" -n "sched-priority": global timeout reached: 20s, ...]
node/desktop-worker uncordoned

Nothing could be evicted. The application is already broken, yet the budget protects the broken Pods and blocks the maintenance. The status explains it:

kubectl get pdb inventory-api-pdb -n sched-priority -o yaml
status:
conditions:
- lastTransitionTime: "2026-10-21T08:29:36Z"
message: ""
observedGeneration: 3
reason: InsufficientPods
status: "False"
type: DisruptionAllowed
currentHealthy: 0
desiredHealthy: 2
disruptionsAllowed: 0
expectedPods: 3
observedGeneration: 3

Step 11 — Deletion Ignores the Budget, AlwaysAllow Fixes Eviction

First, prove that a plain delete is not subject to the budget at all:

kubectl delete pod inventory-api-95745979-h7crt -n sched-priority
pod "inventory-api-95745979-h7crt" deleted from sched-priority namespace

It succeeds immediately, while disruptionsAllowed is 0. That is why PDBs protect against automation and careful operators, not against a careless kubectl delete.

Now the real fix: let not-Ready Pods be evicted.

kubectl patch pdb inventory-api-pdb -n sched-priority --type=merge \
-p '{"spec":{"unhealthyPodEvictionPolicy":"AlwaysAllow"}}'
kubectl drain desktop-worker --pod-selector=app=inventory-api --timeout=30s
kubectl uncordon desktop-worker
node/desktop-worker cordoned
evicting pod sched-priority/inventory-api-95745979-v8w6s
evicting pod sched-priority/inventory-api-95745979-tbqcz
evicting pod sched-priority/inventory-api-95745979-shrmn
pod/inventory-api-95745979-shrmn evicted
pod/inventory-api-95745979-v8w6s evicted
pod/inventory-api-95745979-tbqcz evicted
node/desktop-worker drained
node/desktop-worker uncordoned

The drain completes. Healthy Pods would still have been protected; only the not-Ready ones were let go. For stateless .NET APIs, put unhealthyPodEvictionPolicy: AlwaysAllow in your PDB template from day one.

Step 12 — Seeing the 429 Directly

kubectl drain hides the HTTP details. Switch the policy back and call the Eviction API yourself:

kubectl patch pdb inventory-api-pdb -n sched-priority --type=merge \
-p '{"spec":{"unhealthyPodEvictionPolicy":"IfHealthyBudget"}}'

Create eviction.json with the name of one of the inventory Pods:

{
"apiVersion": "policy/v1",
"kind": "Eviction",
"metadata": {
"name": "inventory-api-95745979-g44nk",
"namespace": "sched-priority"
}
}
kubectl create --raw /api/v1/namespaces/sched-priority/pods/inventory-api-95745979-g44nk/eviction -f eviction.json
Error from server (TooManyRequests): Cannot evict pod as it would violate the pod's disruption budget.

TooManyRequests is HTTP 429. Any tool that evicts Pods, including your own operators written with the Kubernetes .NET client, should treat 429 as "try again later", which is exactly what drain does.

Step 13 — Clean Up

Remove the namespace, the cluster-scoped PriorityClasses, and the fake resource. Removing a capacity entry uses the same JSON Pointer path with remove.

kubectl delete namespace sched-priority
kubectl delete priorityclass lesson-batch-low lesson-online-high lesson-online-high-nonpreempting
kubectl patch node desktop-worker3 --subresource=status --type=json \
-p '[{"op":"remove","path":"/status/capacity/dotnetacademy.dev~1gpu"}]'
kubectl get nodes -o custom-columns=NAME:.metadata.name,UNSCHEDULABLE:.spec.unschedulable

The last command should show <none> for every node, confirming nothing was left cordoned.

Summary

  1. A PriorityClass maps a name to an integer. Higher-priority Pods are scheduled first and may preempt lower-priority Pods when there is no room. Leave system-cluster-critical and system-node-critical to cluster components.
  2. Preemption evicts only lower-priority Pods, picks the node needing the fewest victims, and tries but does not guarantee to respect PDBs. It never helps with selector or taint problems.
  3. preemptionPolicy: Never keeps the queue advantage without evicting anyone.
  4. A fake extended resource on a node status is a safe way to create scarcity for experiments.
  5. A PodDisruptionBudget limits voluntary disruptions through the Eviction API. Prefer maxUnavailable, and make sure replacements can start elsewhere, or drains will stall.
  6. Deletion bypasses PDBs; only eviction respects them. A rejected eviction returns HTTP 429.
  7. kubectl drain --pod-selector limits a drain to specific Pods; always uncordon afterwards.
  8. unhealthyPodEvictionPolicy: AlwaysAllow lets broken, not-Ready Pods be evicted so they cannot block node maintenance.

That completes the section on advanced scheduling. You can now attract Pods to nodes, keep them away, spread them precisely, rank them when resources run out, and protect them while nodes come and go.