Controlling Where Pods Run
Node selectors, affinity, taints and topology spread — and why all three replicas landing on one node is a problem you have to ask for explicitly.
How the Scheduler Decides
For every unscheduled pod, the scheduler runs two stages:
- Filtering — eliminate nodes that cannot host the pod. Not enough allocatable resource (from the requests in the previous guide), an untolerated taint, an unsatisfiable node selector, a volume that can't attach in that zone.
- Scoring — rank the survivors and pick the best. By default it favours spreading pods and prefers nodes that already have the image.
If filtering leaves zero nodes, the pod stays Pending — and the reason is written into its events:
kubectl describe pod api-7d4f8b9c5-x2kfp
# Warning FailedScheduling ... 0/6 nodes are available:
# 3 Insufficient memory, 2 node(s) had untolerated taint {gpu: true}, 1 node(s) had volume node affinity conflict.That message is a precise summary of which filter rejected which nodes. It's the first thing to read for anything Pending, and it usually contains the whole answer.
nodeSelector: the Simple Case
spec:
nodeSelector:
disktype: ssdA hard requirement matched against node labels. No node has the label, the pod never schedules. It's exact-match only — no operators, no preferences.
kubectl get nodes --show-labels
kubectl label node ip-10-0-1-42 disktype=ssdNodes carry useful built-in labels already — kubernetes.io/arch, kubernetes.io/os, topology.kubernetes.io/zone, node.kubernetes.io/instance-type.
Node Affinity: Expressive Requirements
Node affinity does what nodeSelector does, with operators and — crucially — preferences:
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values: [us-east-1a, us-east-1b]
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: node.kubernetes.io/instance-type
operator: In
values: [m6i.xlarge]The field names are long but they parse cleanly:
required...— a hard filter. Unsatisfied means Pending.preferred...— a scoring hint with a weight. Unsatisfied means the pod schedules elsewhere anyway.IgnoredDuringExecution— this is evaluated only at scheduling time. If a node's labels change later, a running pod is not evicted.
Operators available: In, NotIn, Exists, DoesNotExist, Gt, Lt.
Pod Affinity and Anti-Affinity
These place a pod relative to other pods rather than to node labels.
The common and important case is anti-affinity — stopping all your replicas from landing on one node:
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: api
topologyKey: kubernetes.io/hostnametopologyKey defines what "together" means. kubernetes.io/hostname means one pod per node; topology.kubernetes.io/zone means one per zone.
The scheduler's default scoring already spreads pods somewhat, but it's a preference, not a guarantee. On a small or busy cluster, three replicas of a Deployment can absolutely end up on one node — and that node becoming unavailable takes your entire service down. High availability across nodes is something you must ask for explicitly, via anti-affinity or topology spread constraints.
Pod affinity (the positive form) co-locates pods — useful for latency-sensitive pairs, though it's used far less often than anti-affinity.
required pod anti-affinity with topologyKey: kubernetes.io/hostname means you can never have more replicas than nodes. Scale a 3-node cluster's Deployment to 5 and two pods sit Pending forever. Use preferred unless you genuinely need the hard guarantee, or use topology spread constraints, which handle this more gracefully.
Topology Spread Constraints
The modern, and usually better, tool for distribution:
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: apimaxSkew— the maximum permitted difference in pod count between topology domains.1means the most-loaded zone can have at most one more pod than the least.whenUnsatisfiable: DoNotSchedule— hard requirement.whenUnsatisfiable: ScheduleAnyway— best effort; schedule somewhere rather than stay Pending.
Why this beats anti-affinity for most availability work: it expresses even distribution rather than never together, so it degrades sensibly. With ScheduleAnyway, a cluster that can't spread perfectly still runs your pods — instead of leaving them Pending on principle.
Check yourself
A team adds `required` pod anti-affinity with topologyKey kubernetes.io/hostname to guarantee replicas land on different nodes. They later scale from 3 to 6 replicas on a 4-node cluster. What happens?
Taints and Tolerations: the Inverse
Everything so far is the pod choosing a node. Taints let the node repel pods.
kubectl taint nodes gpu-node-1 workload=gpu:NoScheduleNow nothing schedules on gpu-node-1 unless it explicitly tolerates that taint:
spec:
tolerations:
- key: workload
operator: Equal
value: gpu
effect: NoScheduleThree effects:
| Effect | Meaning |
|---|---|
NoSchedule | Don't schedule new pods here |
PreferNoSchedule | Avoid if possible — soft |
NoExecute | Don't schedule, and evict pods already running that don't tolerate it |
A toleration permits but does not attract. A pod tolerating the GPU taint may still be scheduled onto an ordinary node. To both reserve nodes and direct workloads to them, pair a taint (keeps others out) with node affinity (pulls the right workloads in). Using only one is a common half-measure.
Kubernetes uses taints internally too. Control-plane nodes are tainted so general workloads stay off them, and the node controller applies NoExecute taints like node.kubernetes.io/not-ready when a node misbehaves — which is the mechanism that evicts pods from a failing node.
Priority and Preemption
When a cluster is full, priority decides who wins:
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: critical
value: 1000000
globalDefault: false
description: "Revenue-critical services"
---
spec:
priorityClassName: criticalIf a high-priority pod can't schedule, the scheduler may preempt — evict lower-priority pods to make room. Useful for guaranteeing capacity for critical services; also a good way to cause surprise evictions if priorities are handed out casually. Define a small number of classes deliberately.
Check yourself
A node is tainted `workload=gpu:NoSchedule` and a pod has the matching toleration. Where can that pod be scheduled?
What's Next
You can place pods deliberately. The next guide covers changing how many there are — horizontal and vertical autoscaling, and the tuning that stops a scale-up event from taking down the database it was meant to protect.