Taints and tolerations work together to repel Pods from nodes. A taint on a node says "don't schedule here unless you explicitly tolerate me." A toleration on a Pod says "I can handle that taint." This is the opposite of node affinity — affinity attracts Pods to nodes; taints repel Pods from nodes.
1. How Taints & Tolerations Work
Taint Format
# Taint = key=value:effect # Three parts: key, value (optional), effect # Apply a taint: kubectl taint nodes worker-3 gpu=true:NoSchedule # Remove a taint (trailing minus): kubectl taint nodes worker-3 gpu=true:NoSchedule- # View taints: kubectl describe node worker-3 | grep Taints # Taints: gpu=true:NoSchedule
The Three Effects
| Effect | Impact on Scheduling | Impact on Running Pods |
|---|---|---|
NoSchedule | Pod won't be scheduled here (hard) | Existing Pods stay (not evicted) |
PreferNoSchedule | Scheduler tries to avoid, but may place here if no other option | Existing Pods stay |
NoExecute | Pod won't be scheduled here | Existing Pods are evicted (unless they tolerate) |
NoExecute is the only effect that evicts running Pods. NoSchedule and PreferNoSchedule only affect future scheduling — Pods already running on the node when the taint is added continue undisturbed. NoExecute actively removes Pods that don't tolerate it.
2. Writing Tolerations
Exact Match
# Tolerates the specific taint gpu=true:NoSchedule
spec:
tolerations:
- key: "gpu"
operator: "Equal"
value: "true"
effect: "NoSchedule"
Exists Operator (Key-Only Match)
# Tolerates ANY taint with key "gpu" regardless of value:
spec:
tolerations:
- key: "gpu"
operator: "Exists"
effect: "NoSchedule"
# Tolerates ALL taints with key "gpu" (any effect):
- key: "gpu"
operator: "Exists" # no effect specified = matches all effects
# Tolerates ALL taints on ALL keys (nuclear option):
- operator: "Exists" # no key = matches everything
Toleration with tolerationSeconds (for NoExecute)
# "I can tolerate this taint, but only for 300 seconds, then evict me"
spec:
tolerations:
- key: "node.kubernetes.io/unreachable"
operator: "Exists"
effect: "NoExecute"
tolerationSeconds: 300 # Stay for 5 min, then get evicted
Matching Rules
| Operator | Matches When | Value Required? |
|---|---|---|
Equal | Key AND value AND effect all match the taint | Yes |
Exists | Key (and optionally effect) match — value is ignored | No (value field omitted) |
# Summary of matching: # Taint: dedicated=gpu:NoSchedule # # Toleration matches if: # key=dedicated, operator=Equal, value=gpu, effect=NoSchedule ✓ # key=dedicated, operator=Exists, effect=NoSchedule ✓ # key=dedicated, operator=Exists ✓ (any effect) # operator=Exists ✓ (matches all) # key=dedicated, operator=Equal, value=cpu, effect=NoSchedule ✗ (wrong value) # key=other, operator=Equal, value=gpu, effect=NoSchedule ✗ (wrong key)
•
operator: Equal with exact key/value/effect (specific taint)•
operator: Exists with key only (tolerate any value for that key)•
operator: Exists with no key (tolerate everything — used by DaemonSets)
3. Built-in Taints (Kubernetes-Managed)
Kubernetes automatically adds/removes these taints based on node conditions:
| Taint | Added When | Effect |
|---|---|---|
node.kubernetes.io/not-ready | Node condition Ready=False | NoExecute |
node.kubernetes.io/unreachable | Node condition Ready=Unknown (lost contact) | NoExecute |
node.kubernetes.io/memory-pressure | Node has MemoryPressure condition | NoSchedule |
node.kubernetes.io/disk-pressure | Node has DiskPressure condition | NoSchedule |
node.kubernetes.io/pid-pressure | Node has PIDPressure condition | NoSchedule |
node.kubernetes.io/unschedulable | Node is cordoned (kubectl cordon) | NoSchedule |
node-role.kubernetes.io/control-plane | Control plane nodes (kubeadm) | NoSchedule |
Default Tolerations on All Pods
Kubernetes automatically adds these tolerations to every Pod (unless overridden):
# All Pods tolerate not-ready and unreachable for 300s:
tolerations:
- key: node.kubernetes.io/not-ready
operator: Exists
effect: NoExecute
tolerationSeconds: 300
- key: node.kubernetes.io/unreachable
operator: Exists
effect: NoExecute
tolerationSeconds: 300
# This means: if a node goes NotReady, its Pods stay for 5 minutes
# before being evicted. Gives time for transient failures to recover.
unreachable taint for 300s (default). After 5 minutes, the toleration expires and the Pod is evicted (rescheduled elsewhere). You can change this per-Pod by setting a different tolerationSeconds.
4. Common Use Cases
Dedicated Nodes (GPU, High-Memory)
# Taint the GPU nodes:
kubectl taint nodes gpu-worker-1 dedicated=gpu:NoSchedule
kubectl taint nodes gpu-worker-2 dedicated=gpu:NoSchedule
# Only ML Pods tolerate it:
spec:
tolerations:
- key: "dedicated"
operator: "Equal"
value: "gpu"
effect: "NoSchedule"
nodeSelector:
hardware: gpu # Also use nodeSelector to ATTRACT to GPU nodes
# Without nodeSelector, the Pod could land on ANY node
# Toleration only means "I'm allowed on GPU nodes" — not "I must go there"
Maintenance / Drain
# Cordon a node (adds unschedulable taint): kubectl cordon worker-1 # Taint: node.kubernetes.io/unschedulable:NoSchedule # Drain (cordon + evict all Pods): kubectl drain worker-1 --ignore-daemonsets --delete-emptydir-data # DaemonSets tolerate the unschedulable taint automatically # (that's why --ignore-daemonsets is needed for drain)
Spot/Preemptible Nodes
# Cloud providers taint spot instances:
# Taint: cloud.google.com/gke-spot=true:NoSchedule (GKE)
# Taint: node.kubernetes.io/preemptible=true:NoSchedule
# Only cost-tolerant workloads should schedule there:
spec:
tolerations:
- key: "cloud.google.com/gke-spot"
operator: "Equal"
value: "true"
effect: "NoSchedule"
Summary
| Concept | Key Point |
|---|---|
| Taint | Applied to nodes — repels Pods that don't tolerate it |
| Toleration | Applied to Pods — "I can handle this taint" |
| NoSchedule | Don't schedule new Pods; existing Pods stay |
| PreferNoSchedule | Soft preference — avoid if possible |
| NoExecute | Don't schedule + evict existing non-tolerating Pods |
| tolerationSeconds | How long to stay after a NoExecute taint is added |
| Equal operator | Key + value + effect must all match |
| Exists operator | Key matches (any value); omit key to match all taints |
| Toleration ≠ Attraction | Tolerations permit, they don't direct. Combine with nodeSelector/affinity. |
| Built-in taints | K8s auto-taints for not-ready, unreachable, pressure, cordon |
| Default 300s | All Pods tolerate not-ready/unreachable for 5min before eviction |
📝 Quiz: Taints & Tolerations
Q1: A node has taint env=production:NoSchedule. A Pod has no tolerations. Can it schedule there?
key=env, value=production, effect=NoSchedule (or use operator: Exists for the key).Q2: A running Pod is on node-1. You add a NoSchedule taint to node-1. What happens to the Pod?
NoSchedule only affects future scheduling. The running Pod continues undisturbed. Only NoExecute evicts existing Pods. If you want to remove existing Pods, use NoExecute or kubectl drain.Q3: You add a NoExecute taint to node-2. Pod A has a matching toleration with tolerationSeconds: 60. Pod B has no toleration. What happens?
Pod A: Stays for 60 seconds, then is evicted. The
tolerationSeconds gives it a grace period — useful for draining connections gracefully before eviction.Q4: A Pod has tolerations: [{operator: "Exists"}]. What does this tolerate?
Exists operator with no key matches ALL taints on ALL nodes (any key, any value, any effect). The Pod can schedule on any node regardless of taints. This is used by critical system DaemonSets (kube-proxy, CNI) that must run everywhere.Q5: You taint a node for GPU workloads and a Pod has the toleration. But the Pod still schedules on non-GPU nodes. Why?
nodeSelector or nodeAffinity to attract the Pod specifically to GPU nodes.Q6: A node becomes unreachable (network partition). When are its Pods rescheduled to other nodes?
node.kubernetes.io/unreachable:NoExecute. All Pods have a default tolerationSeconds: 300 for this taint. So Pods tolerate it for 300s, then are evicted and rescheduled. Total: ~40s detection + 300s toleration = ~340s.