Kubernetes Taints and Tolerations with Examples

Learn Kubernetes taints and tolerations with practical examples. Test NoSchedule, PreferNoSchedule, NoExecute, nodeSelector, and kubectl taint.

Published

Updated

Read time 14 min read

Reviewed byDeepak Prasad

Kubernetes node taints repelling Pods until a matching Pod toleration allows scheduling
Tested on Rocky Linux 10.2 (Red Quartz) workstation
Package kubectl 1.36.3
Applies to Kubernetes clusters where Node objects can be listed and tainted; control-plane example requires a visible kubeadm control-plane node
Cert prep CKA · CKAD
Lab environment Multi-node kubeadm cluster with containerd — install Kubernetes with kubeadm
Privilege Normal user (no sudo required on the workstation)
Scope Node taints, Pod tolerations, taint effects, kubectl taint, scheduling failures, and DaemonSet control-plane tolerations. Does not cover node affinity, Pod topology spread, or full scheduler configuration.
Related guides Kubernetes pods
Kubernetes API resources
Deployment versus DaemonSet

A node taint marks a scheduling restriction. A Pod toleration is the matching exception. The scheduler evaluates both on every placement attempt. Labels and nodeSelector can narrow placement to a node, but an untolerated NoSchedule or NoExecute taint blocks normal scheduler placement. An untolerated PreferNoSchedule taint only makes the scheduler prefer other nodes. This walkthrough uses the taints-lab namespace so you can see each effect with real kubectl output.

This lab uses k8s-cp as the control-plane node and worker01 as the worker. Run kubectl get nodes and replace these names if your cluster uses different hostnames.


How Taints and Tolerations Work

How the scheduler filters nodes

Placement is not a single decision. The scheduler builds a candidate list, then removes nodes that fail each check:

text
Pod created
  → find nodes that satisfy resource and scheduling requirements
  → apply nodeSelector and node-affinity rules
  → ignore node taints that the Pod tolerates
  → remaining NoSchedule or NoExecute taint excludes the node
  → remaining PreferNoSchedule taint lowers the node's preference
  → an untolerated NoExecute taint can also evict an existing Pod
Mechanism Lives on Question it answers
Label + selector Node label; Pod selector Is this node a candidate for this Pod?
Taint Node Should this node reject Pods that lack permission?
Toleration Pod Does this Pod have permission to ignore a specific taint?

The scheduler evaluates taints when placing a new Pod. For a Pod already running on a node, the taint-eviction-controller enforces an untolerated NoExecute taint.

Scheduler checking a node taint against a Pod toleration before placement

A Pod can match nodeSelector and still stay Pending when the only eligible node carries a taint the Pod does not tolerate. The control-plane lab in the next section demonstrates exactly that sequence.

Labels, selectors, taints, and tolerations

Labels and selectors answer "which objects belong together?" Taints answer "which nodes should reject Pods that are not explicitly allowed?" Selectors and affinity attract Pods to labelled nodes; taints repel Pods, and tolerations permit exceptions.

Mechanism Direction Who sets it Typical use
Label + selector Attracts Admin on nodes or objects Group Pods; Service endpoints; nodeSelector
Taint + toleration Repels (unless tolerated) Admin on nodes; workload author on Pods Dedicated nodes; control-plane isolation; maintenance

A node can have labels and taints at the same time. Meeting a nodeSelector is not enough when an untolerated taint is present.

A taint is a field on Node.spec.taints. Each entry has:

  • key — name of the restriction (for example dedicated or node-role.kubernetes.io/control-plane)
  • value — optional string paired with the key (for example logging)
  • effect — what happens when a Pod lacks a matching toleration

Syntax on the command line:

text
key=value:Effect

When value is empty, omit it:

text
node-role.kubernetes.io/control-plane:NoSchedule

Kubeadm applies that control-plane taint automatically so normal application Pods stay off the control-plane node unless they tolerate it.

A toleration is an entry under spec.tolerations on a Pod (or on a controller template such as a Deployment or DaemonSet). It does not force scheduling onto a node — it only removes the taint as a blocker.

Common fields:

  • key — taint key to match
  • operator — normally Equal or Exists; defaults to Equal
  • value — value compared by Equal; leave it empty when using Exists
  • effect — optional effect to match; when omitted, it matches any effect
  • tolerationSeconds — optional grace period for NoExecute. When omitted from a matching NoExecute toleration, the Pod tolerates that taint indefinitely; 0 causes immediate eviction

Example toleration for the control-plane taint:

yaml
tolerations:
  - key: node-role.kubernetes.io/control-plane
    operator: Exists
    effect: NoSchedule

operator: Exists matches the taint whether or not a value is set — useful for well-known keys such as node-role.kubernetes.io/control-plane.

NoSchedule, PreferNoSchedule, and NoExecute

Effect New Pods without a matching toleration Pods already running on the node
NoSchedule Not scheduled Keep running
PreferNoSchedule Scheduler tries other nodes first; may still place the Pod here Keep running
NoExecute Not scheduled Evicted (respecting tolerationSeconds when set)

NoSchedule is the default mental model for dedicated nodes. NoExecute is what you use when maintenance must clear existing workloads, not only block new ones.


Inspect Taints in the Lab Cluster

Create an isolated namespace for the examples:

bash
kubectl create namespace taints-lab

Sample output:

output
namespace/taints-lab created

List taints on every node:

bash
kubectl get nodes -o custom-columns=NAME:.metadata.name,TAINTS:.spec.taints

Sample output:

output
NAME       TAINTS
k8s-cp     [map[effect:NoSchedule key:node-role.kubernetes.io/control-plane]]
worker01   <none>

The control-plane node carries node-role.kubernetes.io/control-plane:NoSchedule. The worker starts with no custom taints.

For more detail on one node:

bash
kubectl describe node k8s-cp | grep -A2 '^Taints'

Sample output:

output
Taints:             node-role.kubernetes.io/control-plane:NoSchedule
Unschedulable:      false

Unschedulable: false means the node is schedulable in principle — the taint still repels Pods without a toleration.


Test the Control-Plane NoSchedule Taint

Create a Pod without a toleration

Ask the scheduler to place a Pod on the control-plane node using nodeSelector, but do not add a toleration yet:

bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: cp-scheduled
  namespace: taints-lab
spec:
  nodeSelector:
    node-role.kubernetes.io/control-plane: ""
  containers:
    - name: app
      image: nginx:1.27-alpine
EOF

Sample output:

output
pod/cp-scheduled created

Wait until Kubernetes records the scheduling failure:

bash
kubectl wait --for=condition=PodScheduled=false pod/cp-scheduled -n taints-lab --timeout=30s

Sample output:

output
pod/cp-scheduled condition met

Check status:

bash
kubectl get pod cp-scheduled -n taints-lab

Sample output:

output
NAME           READY   STATUS    RESTARTS   AGE
cp-scheduled   0/1     Pending   0          5s

Read the scheduling event:

bash
kubectl describe pod cp-scheduled -n taints-lab | tail -6

Sample output:

output
Events:
  Type     Reason            Age   From               Message
  ----     ------            ----  ----               -------
  Warning  FailedScheduling  5s    default-scheduler  0/2 nodes are available: 1 node(s) didn't match Pod's node affinity/selector, 1 node(s) had untolerated taint(s). no new claims to deallocate, preemption: 0/2 nodes are available: 2 Preemption is not helpful for scheduling.

The message had untolerated taint(s) means the Pod has no toleration for the control-plane taint. The worker did not match the nodeSelector, so only the control-plane node was considered — and the taint blocked placement.

IMPORTANT
Setting spec.nodeName binds a Pod directly to a node and bypasses the scheduler. This means NoSchedule and PreferNoSchedule taints do not block the binding. A NoExecute taint is enforced after binding and can still evict the Pod when no matching toleration exists.

Add the matching toleration

Patch the manifest with a matching toleration and re-apply:

bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: cp-scheduled
  namespace: taints-lab
spec:
  nodeSelector:
    node-role.kubernetes.io/control-plane: ""
  tolerations:
    - key: node-role.kubernetes.io/control-plane
      operator: Exists
      effect: NoSchedule
  containers:
    - name: app
      image: nginx:1.27-alpine
EOF

Sample output:

output
pod/cp-scheduled configured

Wait until the Pod is Ready:

bash
kubectl wait --for=condition=Ready pod/cp-scheduled -n taints-lab --timeout=60s

Sample output:

output
pod/cp-scheduled condition met

Verify placement:

bash
kubectl get pod cp-scheduled -n taints-lab -o wide

Sample output:

output
NAME           READY   STATUS    RESTARTS   AGE   IP               NODE     NOMINATED NODE   READINESS GATES
cp-scheduled   1/1     Running   0          15s   192.168.62.137   k8s-cp   <none>           <none>

The Pod now runs on k8s-cp because the toleration satisfies the control-plane taint. This is the same toleration pattern DaemonSets use when a node agent must reach every node, including control-plane members.


Test a Custom NoSchedule Taint

Block a regular Pod

Reserve worker01 for logging workloads by tainting it:

bash
kubectl taint nodes worker01 dedicated=logging:NoSchedule

Sample output:

output
node/worker01 tainted

Confirm both nodes:

bash
kubectl get nodes -o custom-columns=NAME:.metadata.name,TAINTS:.spec.taints

Sample output:

output
NAME       TAINTS
k8s-cp     [map[effect:NoSchedule key:node-role.kubernetes.io/control-plane]]
worker01   [map[effect:NoSchedule key:dedicated value:logging]]

Deploy a normal Pod with no tolerations:

bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: regular-app
  namespace: taints-lab
spec:
  containers:
    - name: app
      image: nginx:1.27-alpine
EOF

Sample output:

output
pod/regular-app created

Wait until Kubernetes records the scheduling failure:

bash
kubectl wait --for=condition=PodScheduled=false pod/regular-app -n taints-lab --timeout=30s

Sample output:

output
pod/regular-app condition met

Read the scheduling event:

bash
kubectl describe pod regular-app -n taints-lab | tail -5

Sample output:

output
Events:
  Type     Reason            Age   From               Message
  ----     ------            ----  ----               -------
  Warning  FailedScheduling  3s    default-scheduler  0/2 nodes are available: 2 node(s) had untolerated taint(s). no new claims to deallocate, preemption: 0/2 nodes are available: 2 Preemption is not helpful for scheduling.

Both nodes now repel this Pod: the control-plane taint on k8s-cp and dedicated=logging:NoSchedule on worker01.

Schedule a tolerated Pod

Keep the logging taint on worker01, then create a Pod that targets the node and carries the matching toleration:

bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: logging-agent
  namespace: taints-lab
spec:
  nodeSelector:
    kubernetes.io/hostname: worker01
  tolerations:
    - key: dedicated
      operator: Equal
      value: logging
      effect: NoSchedule
  containers:
    - name: app
      image: nginx:1.27-alpine
EOF

Sample output:

output
pod/logging-agent created

Wait until the Pod is Ready:

bash
kubectl wait --for=condition=Ready pod/logging-agent -n taints-lab --timeout=60s

Sample output:

output
pod/logging-agent condition met
bash
kubectl get pod logging-agent -n taints-lab -o wide

Sample output:

output
NAME            READY   STATUS    RESTARTS   AGE   IP            NODE       NOMINATED NODE   READINESS GATES
logging-agent   1/1     Running   0          2s    192.168.5.9   worker01   <none>           <none>

operator: Equal with value: logging matches the taint dedicated=logging:NoSchedule exactly. The nodeSelector narrows placement to worker01; the toleration satisfies the taint so the scheduler can place the Pod there.


Test NoExecute Eviction

NoSchedule only blocks new Pods. NoExecute also removes Pods that are already running.

Clear the earlier lab Pods so worker-app is the only workload from this walkthrough on worker01:

bash
kubectl delete pod regular-app logging-agent -n taints-lab --ignore-not-found

Sample output:

output
pod "regular-app" deleted
pod "logging-agent" deleted

Remove the dedicated taint so worker-app can schedule without a custom toleration:

bash
kubectl taint nodes worker01 dedicated-

Sample output:

output
node/worker01 untainted

Create a Pod on worker01:

bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: worker-app
  namespace: taints-lab
spec:
  nodeSelector:
    kubernetes.io/hostname: worker01
  containers:
    - name: app
      image: nginx:1.27-alpine
EOF

Wait until the Pod is Ready:

bash
kubectl wait --for=condition=Ready pod/worker-app -n taints-lab --timeout=60s

Sample output:

output
pod/worker-app condition met

Review what is already running on the worker before you add the maintenance taint:

bash
kubectl get pods -A --field-selector spec.nodeName=worker01 -o wide

Sample output:

output
NAMESPACE            NAME                                      READY   STATUS    RESTARTS      AGE   IP               NODE       NOMINATED NODE   READINESS GATES
calico-system        calico-node-hc7ph                         1/1     Running   1 (28h ago)   47h   192.168.56.109   worker01   <none>           <none>
calico-system        csi-node-driver-6c9pk                     2/2     Running   2 (28h ago)   47h   192.168.5.3      worker01   <none>           <none>
kube-system          kube-proxy-4m4l7                          1/1     Running   1 (28h ago)   47h   192.168.56.109   worker01   <none>           <none>
local-path-storage   local-path-provisioner-5f584dbd7d-vtvcr   1/1     Running   0             23m   192.168.5.12     worker01   <none>           <none>

Review this list before continuing. The maintenance=down:NoExecute taint can evict other Pods on worker01 when they lack a matching toleration, regardless of namespace. Run this test only on the disposable lab cluster.

NoExecute applies to both newly placed and already-running Pods that do not tolerate the taint.

Add a NoExecute taint:

bash
kubectl taint nodes worker01 maintenance=down:NoExecute

Sample output:

output
node/worker01 tainted

Wait for the Pod to be deleted:

bash
kubectl wait --for=delete pod/worker-app -n taints-lab --timeout=60s

Sample output:

output
pod/worker-app condition met

Confirm it is gone:

bash
kubectl get pod worker-app -n taints-lab

Sample output:

output
Error from server (NotFound): pods "worker-app" not found

The taint-eviction-controller marked and deleted the Pod because it lacked a matching NoExecute toleration. The event reason remains TaintManagerEviction:

bash
kubectl get events -n taints-lab --field-selector involvedObject.name=worker-app,reason=TaintManagerEviction

Sample output:

output
LAST SEEN   TYPE     REASON                 OBJECT           MESSAGE
8s          Normal   TaintManagerEviction   pod/worker-app   Marking for deletion Pod taints-lab/worker-app

To keep a Pod during maintenance, add a toleration for maintenance=down:NoExecute, or remove the taint when work finishes:

bash
kubectl taint nodes worker01 maintenance-

Test PreferNoSchedule

Apply a soft PreferNoSchedule taint:

bash
kubectl taint nodes worker01 dedicated=logging:PreferNoSchedule

Deploy a Pod with no tolerations:

bash
kubectl delete pod regular-app -n taints-lab --ignore-not-found
bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: prefer-demo
  namespace: taints-lab
spec:
  containers:
    - name: app
      image: nginx:1.27-alpine
EOF

Wait until the Pod is Ready:

bash
kubectl wait --for=condition=Ready pod/prefer-demo -n taints-lab --timeout=60s

Sample output:

output
pod/prefer-demo condition met
bash
kubectl get pod prefer-demo -n taints-lab -o wide

Sample output:

output
NAME          READY   STATUS    RESTARTS   AGE   IP             NODE       NOMINATED NODE   READINESS GATES
prefer-demo   1/1     Running   0          2s    192.168.5.11   worker01   <none>           <none>

The scheduler placed the Pod on worker01 even without a toleration because PreferNoSchedule is a hint, not a hard block. The control-plane NoSchedule taint still kept k8s-cp unavailable, so the worker was the only viable choice. On a larger cluster with many untainted workers, this Pod would likely avoid the PreferNoSchedule node.


Default NotReady and Unreachable Tolerations

By default, Kubernetes adds NotReady and Unreachable tolerations to Pods that do not already define tolerations for those taints. Inspect prefer-demo, which has no custom tolerations:

bash
kubectl get pod prefer-demo -n taints-lab -o jsonpath='{range .spec.tolerations[*]}{.key}{" "}{.effect}{" seconds="}{.tolerationSeconds}{"\n"}{end}'

Sample output:

output
node.kubernetes.io/not-ready NoExecute seconds=300
node.kubernetes.io/unreachable NoExecute seconds=300

The defaults come from the DefaultTolerationSeconds admission controller and normally use 300 seconds, although a cluster administrator can change those values.


nodeSelector vs taints and tolerations

These mechanisms answer different questions and are often confused because both affect where a Pod lands.

Mechanism Set on Direction Question it answers
nodeSelector Pod Attracts / requires Which labelled nodes may this Pod use?
Taint Node Repels Which Pods should stay off this node?
Toleration Pod Permits exception May this Pod ignore a specific taint?

nodeSelector is a Pod-side requirement. The scheduler only considers nodes whose labels match. It does not grant permission to run on a tainted node.

A taint is a node-side restriction. The cluster admin marks a node as reserved, maintenance-bound, or control-plane-only. Most Pods are turned away unless they carry a matching toleration.

A toleration alone does not schedule a Pod onto a node. It only stops a taint from blocking placement. The logging-agent lab used both: nodeSelector picked worker01, and the toleration satisfied dedicated=logging:NoSchedule.

Goal Use
Run this Pod only on GPU nodes Label GPU nodes; set nodeSelector or affinity on the Pod
Keep most Pods off the control plane Taint control-plane nodes; let system components add tolerations
Reserve a worker for logging agents Taint the worker; add matching toleration only on logging Pods
Run a DaemonSet on every node including control plane DaemonSet tolerations for control-plane taints — no nodeSelector required for that part
Drain a node and evict existing Pods NoExecute taint — nodeSelector cannot do this

When a Pod stays Pending, read the event carefully. didn't match node affinity/selector means the Pod's placement rule failed. had untolerated taint(s) means the node was a candidate but the Pod lacks permission. Both can appear in one message, as you saw with cp-scheduled.

For label and selector syntax detail, see Kubernetes labels and selectors. Node affinity and topology spread add softer or spread-based placement rules; this article stays with nodeSelector and taints.


Taints with DaemonSets

DaemonSet node agents often need tolerations for control-plane or dedicated taints. Add them under spec.template.spec.tolerations — the same location as a standalone Pod. Without a control-plane toleration, DESIRED stays below total node count because tainted nodes are excluded.

The previous section explains when to pair tolerations with nodeSelector; DaemonSets are the common case where tolerations alone let a per-node agent reach tainted control-plane nodes.


Troubleshooting

Symptom Likely cause Fix
Pod stays Pending; events mention untolerated taint Pod lacks toleration for a node taint Add matching spec.tolerations or remove the taint
Pod stays Pending; only didn't match node affinity/selector nodeSelector or affinity does not match any node Fix labels on nodes or relax selector rules
Pod runs on a tainted node although its original YAML has no toleration Pod existed before a NoSchedule taint, used nodeName, or received an automatic/injected toleration Inspect the live Pod with kubectl get pod <name> -o yaml; compare its creation time with the taint and check spec.tolerations
DaemonSet DESIRED less than node count Tainted nodes excluded (common on control-plane) Add toleration on DaemonSet template
Pod evicted shortly after node taint added NoExecute effect Add NoExecute toleration or remove taint after maintenance
kubectl taint reports taint already exists Duplicate key/effect on node Use --overwrite or remove with key- first

NoSchedule does not evict Pods already running, and DaemonSet Pods automatically receive tolerations for several node-condition taints.


What's Next


References


Summary

Taints are repulsion rules on nodes; tolerations are matching exceptions on Pods. The scheduler checks both before it places a workload — selectors and nodeSelector do not override an untolerated NoSchedule or NoExecute taint. Kubernetes treats PreferNoSchedule as a soft preference rather than a scheduling prohibition. In the lab you saw a Pod stay Pending on the control-plane node until a toleration matched node-role.kubernetes.io/control-plane:NoSchedule, a custom dedicated=logging:NoSchedule taint reserve a worker for specialised Pods, and NoExecute evict a running Pod through TaintManagerEviction.

Remember the one-line rule: selectors attract, taints repel, tolerations permit. NoSchedule blocks new Pods, PreferNoSchedule nudges the scheduler away unless the cluster is tight, and NoExecute can evict Pods that are already running. For node-level agents that must reach every node including the control plane, carry tolerations on the DaemonSet Pod template — the DaemonSet walkthrough applies the control-plane toleration after you understand the model here.


Frequently Asked Questions

1. What is the difference between a Kubernetes taint and a label?

A label describes an object so selectors can match it. A taint on a node repels Pods that lack a matching toleration. Selectors and affinity attract Pods to labelled nodes; taints repel Pods, and tolerations permit exceptions.

2. Do I need a toleration for every taint on a node?

A Pod needs a toleration for each taint that would otherwise block placement. Untolerated NoSchedule and NoExecute taints block scheduling, while an untolerated PreferNoSchedule taint only makes the scheduler try other nodes first.

3. What does NoSchedule mean on a taint?

NoSchedule blocks new Pods without a matching toleration from scheduling onto that node. Pods already running are not evicted.

4. What does NoExecute do?

NoExecute blocks new Pods without a matching toleration and evicts Pods already running on the node when the taint is added or when their toleration expires.

5. Why can a Pod land on a tainted control-plane node when I set nodeName?

nodeName binds the Pod directly and bypasses scheduler checks, so NoSchedule and PreferNoSchedule do not prevent placement. A NoExecute taint can still evict the Pod unless it has a matching toleration.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)