Skip to main content
RunBook Academy

KubernetesXXVI · Taints and TolerationsScheduling and node lifecycle

Effect types — NoSchedule, PreferNoSchedule, NoExecute

Advanced⏱ ~17 minkubectl

What you'll learn

  • Distinguish the three effect types operationally
  • Use PreferNoSchedule as a soft hint versus a hard rule
  • Apply NoExecute with tolerationSeconds for staged eviction
  • Combine effects on the same node for progressive maintenance

Prerequisites

Verified against Kubernetes 1.34.x · kubeadm 1.34.x · kubectl 1.34.x · etcd 3.6.x · CoreDNS 1.11.x · containerd 1.7.x / 2.x · 2026-08-16

Not yet marked complete on this device.

The effect field is the only part of a taint that changes the user-visible behaviour. This lesson walks the three values, the operational differences between them, and the patterns that arise from combining them.

NoSchedule — the hard filter

NoSchedule is the default effect and the most common. A taint with effect: NoSchedule causes the scheduler’s TaintToleration filter plugin to reject the node for any Pod that does not have a matching toleration. The Pod stays in the unscheduled queue; if no other node is feasible, the Pod stays Pending with a FailedScheduling event.

apiVersion: v1
kind: Pod
metadata:
  name: web-1
spec:
  containers:
    - name: app
      image: registry.example.com/web:1.0.0
  tolerations:
    - key: dedicated
      operator: Equal
      value: prod
      effect: NoSchedule

The Pod is allowed onto nodes with the matching taint; it is also allowed onto nodes with no taint at all. The taint constrains; it does not require.

A NoSchedule taint does not affect running Pods. If a Pod is already on the node when the taint is added, the Pod continues running, the kubelet does not evict it, and the next reconciliation cycle does not move it. The Pod becomes a “tainted” running workload that no longer has matching scheduling criteria. This is the property that makes NoSchedule safe for staged maintenance.

PreferNoSchedule — the soft hint

PreferNoSchedule is the same logic as NoSchedule from the filter plugin’s perspective, except the scheduler does not reject the node for the mismatch. Instead, the scheduler decreases the node’s score by a small amount (currently NodeTaintPolicy weight, default 1 in the scoring weights).

flowchart LR
    A[Pod submitted] --> B[Filter plugins]
    B --> C[Feasible set]
    C --> D[Score plugins]
    D -->|PreferNoSchedule reduces score| E[Winning node probably not tainted]
    E --> F[Bind]

The result: the scheduler tries to avoid the tainted node, but if no other node is feasible, the tainted node wins. This is the difference between “do not schedule here” and “do not schedule here unless you must.”

When to use PreferNoSchedule:

  • Soft isolation. A node pair carrying data=local:PreferNoSchedule is a hint that the cluster operator would prefer to keep general workloads off these nodes, but tolerating them is acceptable when capacity is thin.
  • Canary rollouts. New nodes can be tainted with preflight=run:PreferNoSchedule so the scheduler prefers them for new Pods, but production workloads do not fail to schedule if the canary set is too small.
  • Soft affinity. If you want to encourage Pods onto a node class but not block them, PreferNoSchedule is the right shape.

When not to use PreferNoSchedule:

  • Compliance or security boundaries. The scheduler does not guarantee placement; it just prefers it. A pod that explicitly tolerates the taint or that has no other option lands on the node anyway.
  • Cost-bound nodes. A spot-instance node tainted cloud=spot:PreferNoSchedule does not protect against a spot interruption; the taint is a placement hint, not a scheduling policy.

NoExecute — the eviction lever

NoExecute is the only effect that removes running Pods. When a NoExecute taint is added to a node, the kubelet’s node-level controller (the “node lifecycle controller” in kube-controller-manager, plus the per-node eviction logic in kubelet) walks the node’s running Pods and evicts any Pod whose tolerations do not match the taint.

The eviction is graceful by default. The Pod’s containers receive SIGTERM, the kubelet waits for the Pod’s terminationGracePeriodSeconds (default 30s), then sends SIGKILL. The Pod’s status moves to Failed with reason Evicted. The replacement Pod, if one is managed by a controller, is scheduled by the scheduler under the same constraints.

NoExecute is the most production-loaded effect. Adding NoExecute to a node with running Pods is a destructive operation; in production, the operation must be staged.

tolerationSeconds

A toleration can specify how long the Pod tolerates the taint before eviction. The order of precedence is:

  1. The Pod’s tolerationSeconds for the matching taint
  2. The taint’s tolerationSeconds (set on the taint itself)
  3. The kubelet’s default eviction grace period (300 seconds)
tolerations:
  - key: node.kubernetes.io/unreachable
    operator: Exists
    effect: NoExecute
    tolerationSeconds: 30

This declares “if the node goes unreachable, evict me after 30 seconds”. Most operational taints are tolerated for 300 seconds by default; shorter values make the cluster more sensitive to transient network partitions, longer values ride out brief blips.

Combining effects for staged maintenance

The three effects let the operator build a progression:

stateDiagram-v2
    [*] --> Operating
    Operating --> NoSchedule: taint NoSchedule<br/>no new Pods
    NoSchedule --> NoExecute: taint NoExecute<br/>drain via tolerationSeconds
    NoExecute --> Maintenance: all pods evicted
    Maintenance --> Operating: remove taints

The pattern, in production:

  1. Apply node.kubernetes.io/unschedulable:NoSchedule (via kubectl cordon) — the node is no longer a candidate for new Pods. Existing Pods continue running.
  2. Schedule the maintenance. The window is now self-paced; you can wait for the existing Pods to finish naturally.
  3. If you need to force the existing Pods off the node (rolling reboot, kernel upgrade), apply a second NoExecute taint with a finite tolerationSeconds so the Pods drain gracefully.

Adding PreferNoSchedule to the same node is almost never necessary; if the node is cordoned, the scheduler will not consider it for new Pods.

# A Pod stuck in Pending, from `kubectl get pods --field-selector status.phase=Pending`:
PENDING_POD=billing-7d8f-abcde

kubectl describe pod "$PENDING_POD" | grep -A 3 "Events:"
Events:
  Type     Reason            Age   From               Message
  ----     ------            ----  ----               ----
  Warning  FailedScheduling  12s   default-scheduler  0/5 nodes are available:
                                            1 node(s) had taint {dedicated=prod:NoSchedule},
                                            2 node(s) had taint {gpu=true:NoSchedule},
                                            2 node(s) didn't match Pod's node affinity.

The events list every taint that rejected every node. The fix is either to add the tolerations to the Pod, remove the taint from the node, or change the Pod’s deployment so it runs on a different node class.

Quiz

Knowledge check · 4 questions

  1. Q1. Which effect causes Pods that are already running on the node to be removed?

  2. Q2. Adding a `NoSchedule` taint to a node evicts the Pods already running on it.

  3. Q3. Explain why a node tainted an hour ago is still running the workloads the taint was meant to remove.

    Before a kernel upgrade, an operator ran `kubectl taint nodes node-4 maintenance=true:NoSchedule` and left the window open for an hour. `kubectl get pods -o wide --field-selector spec.nodeName=node-4` still lists 14 running Pods, none of which tolerate the `maintenance` key. The host is due to be rebooted in 10 minutes.

  4. Q4. Why is `PreferNoSchedule` unsuitable for enforcing a compliance boundary, and what does the scheduler actually do with it?

Passing score: 75%. Answers are checked in this browser.

Production discipline

  • Use NoSchedule for hard isolation. Infrastructure nodes, GPU nodes, and security-sensitive workloads should carry NoSchedule taints that the appropriate Pods tolerate.
  • Use PreferNoSchedule sparingly. A soft hint becomes noise if it is overridden most of the time. If the Pod always tolerates the taint or always runs on the node despite the hint, the taint has no effect.
  • Treat NoExecute as a write operation. Adding NoExecute to a node evicts running Pods. Stage it. Use kubectl drain, which adds NoSchedule first, then NoExecute with a tolerance, after the cluster has time to reschedule.
  • Reserve tolerationSeconds for batch and stateless workloads. A long tolerationSeconds on a stateful workload means the workload tolerates a node failure for that long before eviction; this can hide a slow-fail node, prolonging a partition.
  • Audit taints at every node repave. A taint set on a node that the new node does not inherit is a forgotten thing. Drive taints from the cluster bootstrap (cloud-init, kubelet flags, or a node bootstrap controller) so they survive node replacement.