nodeSelector and Scheduling Constraints
Two Pods with an identical nodeSelector pointing at the same node. One runs, one stays Pending forever. The difference is three lines the node itself put in the way - and the distinction between what a Pod asks for and what a node accepts is most of what scheduling is.
Scheduling and Placement Guide 14 of 46 Beginner
- Kubernetes1.36.4
- Cluster4 nodes
- Runtimecontainerd 2.2.6
- CNICalico v3.32.1
- TimeAbout 16 min
- Reviewed22 August 2026
Written against the versions above. Labels, nodeSelector, affinity and taints are core Kubernetes and behave the same on any conformant cluster. The control plane's own node-role.kubernetes.io/control-plane taint is a kubeadm convention - managed services usually hide the control plane from you entirely.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| CKA1001 | 192.168.0.175 | Ubuntu 26.04 LTS | Control Plane Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE01 | 192.168.0.176 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE02 | 192.168.0.177 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE03 | 192.168.0.178 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
Before you start
- A cluster with at least two worker nodes, and permission to label and taint them. Both are cluster-scoped writes.
- Nothing is installed. Every object in this session is created and read through kubectl.
-
Nodes are labelled, and selectors are how you talk about them
Scheduling starts with labels, because that is the only vocabulary the scheduler has for describing a node. Put two on a worker:
node/cka1001-node01 labeledNow the same selector syntax that works on Pods works on nodes. Equality, a column view, set membership, and existence:
kubectl get nodes -l disk=ssd # just node01 kubectl get nodes -L disk,tier # all nodes, labels as columns kubectl get nodes -l "disk in (ssd,nvme)" # set membership kubectl get nodes -l "!disk" # the three WITHOUT the label-lfilters,-Ldisplays. The!diskform is the one worth remembering: selectors can ask for the absence of a label, which is how you find the nodes a placement rule will not match. Every node already carries labels nobody set -kubernetes.io/hostname,kubernetes.io/arch,kubernetes.io/os- and those are usable in exactly the same way.bash Example session kubectl label node cka1001-node01 disk=ssd tier=fastnode/cka1001-node01 labeledkubectl get nodes -l disk=ssdNAME STATUS ROLES AGE VERSIONcka1001-node01 Ready <none> 150m v1.36.4kubectl get nodes -L disk,tierNAME STATUS ROLES AGE VERSION DISK TIERcka1001 Ready control-plane 151m v1.36.4 cka1001-node01 Ready <none> 150m v1.36.4 ssd fastcka1001-node02 Ready <none> 150m v1.36.4 cka1001-node03 Ready <none> 85m v1.36.4 kubectl get nodes -l "!disk"NAME STATUS ROLES AGE VERSIONcka1001 Ready control-plane 151m v1.36.4cka1001-node02 Ready <none> 150m v1.36.4cka1001-node03 Ready <none> 85m v1.36.4Expected resultOne node carries the new labels; three do not, and a selector can find either set.
Success conditionYou can filter nodes by a label and by the absence of one.
-
nodeSelector is a requirement; affinity can be a preference
A Deployment with
nodeSelector: {disk: ssd}and three replicas. All three land on the one node that has the label:3 cka1001-node01nodeSelectoris absolute. If no node matches, the Pods do not go anywhere - they stay Pending indefinitely, which is one of the two Pending causes worth recognising on sight.Node affinity says the same kind of thing with a dial. This second Deployment uses
preferredDuringSchedulingIgnoredDuringExecutionwith weight 100, so the scheduler *tries* for the ssd node and settles for anything if it cannot. Four replicas:4 cka1001-node01Same outcome, and that is the point worth sitting with - an identical result proves nothing about which mechanism you used. The preference was satisfiable here because node01 had room. Fill that node and the preferred version would spread to the others while the nodeSelector version would go Pending. The difference only shows up under pressure, which is exactly when you need to know which you wrote.
That long field name is worth decoding: it applies
DuringSchedulingand isIgnoredDuringExecution- so a Pod already running is never moved because a label changed.bash Example session kubectl get pods -l app=pinned -o jsonpath="{range .items[*]}{.spec.nodeName}{\"\n\"}{end}" | sort | uniq -c 3 cka1001-node01kubectl get pods -l app=affinity-demo -o jsonpath="{range .items[*]}{.spec.nodeName}{\"\n\"}{end}" | sort | uniq -c 4 cka1001-node01Expected resultBoth Deployments place every replica on the labelled node.
Success conditionYou can say what would differ between the two if that node were full.
-
A taint is the node's own refusal
Everything so far is the Pod choosing. A taint is the other direction: the node refusing Pods that have not explicitly accepted it.
node/cka1001-node02 taintedNote what the taint list shows - the control plane has carried one since installation:
cka1001 node-role.kubernetes.io/control-plane cka1001-node02 dedicatedThat is the real answer to "why do my Pods never run on the control plane". Not a special rule - just a taint, the same mechanism you can apply yourself.
Now the pair this guide is named after. Two Pods, the same
nodeSelector: {kubernetes.io/hostname: cka1001-node02}, differing only in that the second carries a toleration:intolerant Pending <none> tolerant Running cka1001-node02The event on the failed one counts it out loud:
0/4 nodes are available: 2 node(s) didn't match Pod's node affinity/selector, 2 node(s) had untolerated taint(s).Two rejected for the wrong hostname, two for taints - the control plane and the one just tainted. Its selector pointed at a node that would have taken it on every count except one. Asking for a node is not the same as being accepted by it.
bash Example session kubectl taint node cka1001-node02 dedicated=batch:NoSchedulenode/cka1001-node02 taintedkubectl get nodes -o custom-columns=NAME:.metadata.name,TAINTS:.spec.taints[*].keyNAME TAINTScka1001 node-role.kubernetes.io/control-planecka1001-node01 <none>cka1001-node02 dedicatedcka1001-node03 <none>kubectl get pod intolerantNAME READY STATUS RESTARTS AGEintolerant 0/1 Pending 0 12skubectl get events --field-selector involvedObject.name=intolerant,reason=FailedScheduling | tail -2LAST SEEN TYPE REASON OBJECT MESSAGE12s Warning FailedScheduling pod/intolerant 0/4 nodes are available: 2 node(s) didn't match Pod's node affinity/selector, 2 node(s) had untolerated taint(s). no new claims to deallocate, preemption: 0/4 nodes are available: 4 Preemption is not helpful for scheduling.kubectl wait --for=condition=Ready pod/tolerant --timeout=180spod/tolerant condition metkubectl get pods intolerant tolerant -o custom-columns=NAME:.metadata.name,STATUS:.status.phase,NODE:.spec.nodeNameNAME STATUS NODEintolerant Pending <none>tolerant Running cka1001-node02Expected resultIdentical selectors, one Pod Pending and one Running, with the taint named in the event.
Success conditionYou can explain the Pending Pod without looking at its image or its logs.
-
Priority decides who gets evicted when there is no room
The last piece KCNA expects you to recognise. A PriorityClass is a named number, and a Pod that references one is scheduled ahead of lower-priority Pods - and can cause them to be evicted to make room:
NAME VALUE GLOBAL-DEFAULT AGE PREEMPTIONPOLICY demo-low 100 false 0s PreemptLowerPriority demo-high 1000000 false 0s PreemptLowerPriorityglobalDefault: falseon both, so nothing changes for Pods that do not ask.PreemptLowerPriorityis the default policy: this priority may evict things below it. The alternative,Never, jumps the scheduling queue without evicting anyone.Clusters ship with their own classes -
system-cluster-criticalat two billion - which is how the control plane's own workloads outrank anything you create. The numbers are meaningless in isolation and only matter relative to each other, which is why picking round numbers far apart is the convention.bash Example session kubectl get priorityclass demo-low demo-highNAME VALUE GLOBAL-DEFAULT AGE PREEMPTIONPOLICYdemo-low 100 false 0s PreemptLowerPrioritydemo-high 1000000 false 0s PreemptLowerPriorityExpected resultTwo PriorityClasses, neither of them a global default.
Success conditionYou can say what PreemptLowerPriority permits.
Troubleshooting
A Pod is Pending and the event says "didn't match Pod's node affinity/selector".
Why: Its nodeSelector or required affinity names labels no node carries. nodeSelector is a hard requirement, not a hint.
Fix:Compare the two directly:
kubectl get pod <p> -o jsonpath='{.spec.nodeSelector}'againstkubectl get nodes --show-labels. Either label a node or relax the rule to a preferred affinity.A Pod is Pending and the event says "had untolerated taint(s)".
Why: The nodes that would otherwise fit are tainted and the Pod carries no matching toleration.
Fix:List them with
kubectl get nodes -o custom-columns=NAME:.metadata.name,TAINTS:.spec.taints[*].key, then add a toleration for the key and effect - or remove the taint withkubectl taint node <n> <key>-.Nothing ever schedules onto the control-plane node.
Why: It carries the node-role.kubernetes.io/control-plane taint from installation. Working as designed.
Fix:Nothing, normally. Only on a single-node cluster is removing that taint reasonable, and it means your workloads compete with the API server for CPU.
A Pod is running on the wrong node and changing the node's labels does not move it.
Why: Affinity is IgnoredDuringExecution - the rule is applied when the Pod is scheduled and never re-evaluated.
Fix:Delete the Pod and let its controller recreate it. There is no in-place reschedule.