Sign in
← Kubernetes: scheduling
6 min · medium · Lesson 6 of 7

Taints and nodeAffinity: pass and address

Why a toleration without affinity doesn't keep a Pod pinned to a dedicated node, and why affinity without a toleration leaves it stuck Pending.

Why a toleration without nodeAffinity doesn't keep a Pod on the dedicated node, and nodeAffinity without a toleration leaves it Pending.

Why compare taints and affinity

At first glance they do the same thing: restrict where a Pod runs. In practice they work from two different sides, which is exactly why they're often used together.

Short rule:

Taint = pass: only a Pod with a matching toleration is let onto the node. Affinity = address: the Pod itself looks for a node with the right label.

To make sure a Pod lands on a dedicated node and doesn't drift elsewhere, you need both mechanisms. Either one alone breaks.

MechanismWho sets itWhat it givesWhat it doesn't give
taint + tolerationadmin on the node, developer in the Podlets the Pod onto a tagged nodedoesn't pull the Pod toward it
label + nodeAffinityadmin on the node, developer in the Podaims the Pod at a nodedoesn't remove the taint

A taint and a label on a node are independent of each other. Setting a taint doesn't create a label, and the other way around.

Toleration only: the Pod goes wherever

A toleration lets a Pod ignore a taint. It doesn't tell the scheduler "put me here", only "if you put me here, I'm fine with it".

In a cluster with several nodes, a Pod with a toleration but no affinity can land on any node that passes the checks. Nodes without taints suit it just as well as the dedicated one. The dedicated node stays empty, even though resources were set aside for it.

Affinity only: the Pod has the address but isn't let in

Affinity does the opposite: it requires a node with the right label, but doesn't remove the taint from it. The scheduler checks every node against every filter. A node without the label drops out on affinity. A node with the label drops out on a taint the toleration doesn't cover. No nodes are left, the Pod sits Pending.

This is a classic trap: the Pod's affinity is correct, the node's label is correct, and the Pod is still Pending. The cause is a taint the toleration doesn't cover: it's missing entirely, or one of its fields doesn't match the taint.

In the FailedScheduling event, the scheduler gives a separate reason for each node, for example 1 node(s) didn't match Pod's node affinity/selector, 1 node(s) had untolerated taint(s). People often read only the affinity part and don't get to the second half of the line.

The full set

spec:
  tolerations:
    - key: dedicated
      operator: Equal
      value: critical
      effect: NoSchedule
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions:
              - key: dedicated
                operator: In
                values: [critical]

The Pod tolerates the taint dedicated=critical:NoSchedule and itself looks for a node with the label dedicated=critical. Both requirements point to the same node, and the Pod passes both filters. In a Deployment, this block lives in spec.template.spec, next to containers.

A nodeSelector (see the nodeSelector lesson) can serve as the address instead of required nodeAffinity: for a single exact label match, it's enough.

Scenario: a GPU pool for ML only

A typical setup:

  1. The admin sets a taint: kubectl taint nodes gpu-pool dedicated=gpu:NoSchedule. Random Pods with no toleration won't land here.
  2. The admin sets a label: kubectl label nodes gpu-pool dedicated=gpu. It can be used as an affinity target.
  3. ML apps add both a toleration for dedicated=gpu:NoSchedule and nodeAffinity on dedicated In [gpu] to their Pods.

The GPU nodes are protected from outsiders, and the ML Pods land on the right hardware.

How the scheduler handles them

kube-scheduler picks a node in two stages: filtering and scoring.

Filtering drops nodes the Pod can't land on:

Scoring gives points to the nodes that remain:

If filtering drops every node, the Pod stays Pending. If several nodes pass, the one with the highest score is picked.

In a single-node cluster you only see half the picture. The filter on taints and required affinity works, but the "toleration present, affinity missing" mismatch never shows up: the Pod has nowhere else to go anyway. The difference between the two mechanisms only shows when there are at least two nodes. That's why this lesson's labs bring up a two-node cluster.

Common mistakes

kubectl taint nodes <name> <key>=<value>:<effect>
kubectl label nodes <name> <key>=<value>
kubectl get nodes -L <label-key>
kubectl describe node <name> | grep -A3 Taints
kubectl describe pod <name> | grep -A10 Events
kubectl explain pod.spec.tolerations
kubectl explain pod.spec.affinity.nodeAffinity

What you'll need for the tasks

The tasks are in the labs/ folder: each lab brings up its own two-node kind cluster on your machine, instructions in its README.md.

In short

Done reading?

Mark the lesson as read to move your track progress forward.

Go through it with an AI agent

The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.

Get access for $49

Tell the agent: use MCP izi, give me lesson k8s-scheduling-06-taints-vs-affinity