Skip to content
Documentation Background

Scheduling

The kube-scheduler watches for newly created Pods that have no node assigned and selects the best-fit node for each one. Understanding how it makes that decision — and how to constrain or influence it — is central to running reliable production workloads.


The scheduler runs a continuous loop and evaluates every unscheduled Pod using a strict two-step process:

How the Scheduler Works

Eliminates nodes that cannot run the Pod. Checks include:

  • Node has enough unallocated CPU and memory (requests)
  • Node satisfies nodeSelector labels
  • Node satisfies required affinity rules
  • Node does not carry an unmatched taint (with NoSchedule effect)
  • Node passes other hard constraints (volume topology, port conflicts, etc.)

Ranks the surviving candidate nodes. Common scoring factors:

  • Spreading pods across nodes/zones
  • Preferring nodes with more remaining resources
  • Soft affinity preferences

The node with the highest score wins. The scheduler writes the chosen node name into the Pod’s spec.nodeName and the kubelet on that node picks up the pod.

If no node survives the filter step, the pod stays Pending until the loop re-evaluates.


Three ways to find which node a Pod was placed on:

Terminal window
# 1. Wide output — adds NODE and IP columns
kubectl get pod nginx -o wide
# 2. Filter YAML — prints the specific field
kubectl get pod nginx -o yaml | grep nodeName
# 3. Describe — top section shows "Node: <name>/<ip>"
kubectl describe pod nginx

Setting spec.nodeName directly in a manifest skips filtering and scoring entirely — the pod is assigned to that node unconditionally:

spec:
nodeName: worker-node-02 # bypasses scheduler — use only for debugging
containers:
- name: app
image: nginx:1.27.1

A nodeSelector is a hard requirement — the pod can only be scheduled on nodes that have all specified labels. No match → pod stays Pending.

Terminal window
# Label a node first
kubectl label node worker-node-03 disk=ssd
# Verify
kubectl get nodes --show-labels | grep disk=ssd
Node Selector
spec:
nodeSelector:
disk: ssd # must match node label exactly (key=value)
kubernetes.io/os: linux # can combine multiple labels — all must match
containers:
- name: app
image: nginx:1.27.1
CharacteristicDetail
SemanticsLogical AND — all labels must be present on the node
FlexibilityExact key=value match only — no operators or ranges
On no matchPod stays Pending
MutableChanging node labels in/out of scope triggers pod creation/deletion

Kubernetes and cloud providers automatically apply a set of standard labels to every node — you can use these in nodeSelector or affinity rules without manually labelling nodes first:

LabelApplied byCommon use case
kubernetes.io/hostnameKubernetesPer-node topology key for spreading
kubernetes.io/osKubernetesWindows/Linux targeting (linux, windows)
kubernetes.io/archKubernetesCPU architecture (amd64, arm64)
topology.kubernetes.io/zoneCloud providersZone-level HA and spreading
topology.kubernetes.io/regionCloud providersRegion-level targeting
node.kubernetes.io/instance-typeCloud providersGPU, spot, or specific instance targeting
Terminal window
# Inspect all labels on a node
kubectl get node <name> --show-labels

Node affinity extends nodeSelector with operators and soft preferences. It lives under spec.affinity.nodeAffinity.

Node Affinity
TypeShort nameEffect
requiredDuringSchedulingIgnoredDuringExecutionrequiredHard rule — pod is not scheduled if no node matches
preferredDuringSchedulingIgnoredDuringExecutionpreferredSoft rule — scheduler tries its best but won’t block scheduling

IgnoredDuringExecution on both types means: already-running pods are never evicted if node labels change after scheduling.

OperatorMeaning
InNode label value is in the specified set
NotInNode label value is not in the specified set
ExistsNode has the label key (any value, or no value needed)
DoesNotExistNode does not have the label key
GtNode label value is numerically greater than the specified value
LtNode label value is numerically less than the specified value
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: disk
operator: In
values:
- ssd
- nvme # pod can run on any node with disk=ssd OR disk=nvme
containers:
- name: app
image: nginx:1.27.1
spec:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80 # higher weight = stronger preference (1–100)
preference:
matchExpressions:
- key: disk
operator: In
values:
- ssd
- weight: 20
preference:
matchExpressions:
- key: zone
operator: In
values:
- us-east-1a

Achieved with negating operators (NotIn, DoesNotExist):

spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: disk
operator: NotIn
values:
- spinning-disk # repel from nodes with spinning-disk label

Where node affinity targets specific nodes by their labels, pod affinity targets other pods — it tells the scheduler to place a pod near (or away from) pods with a given label. The topologyKey defines what “near” means.

  • podAffinity → “schedule me on a node that also runs pods matching label X”
  • podAntiAffinity → “schedule me on a node that does not run pods matching label X”

Both support the same required/preferred types as node affinity.

Pod Affinity — Co-locate pods (e.g. app + cache)

Section titled “Pod Affinity — Co-locate pods (e.g. app + cache)”
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: cache # must co-locate with pods labelled app=cache
topologyKey: kubernetes.io/hostname # "same node" = same hostname value

Pod Anti-Affinity — Spread replicas across nodes

Section titled “Pod Anti-Affinity — Spread replicas across nodes”
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: web # avoid nodes that already run an app=web pod
topologyKey: kubernetes.io/hostname # enforce one replica per node

Swap topologyKey to topology.kubernetes.io/zone to spread across zones instead of nodes.

spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: web
topologyKey: kubernetes.io/hostname

Affinity pulls pods toward nodes. Taints repel pods from nodes. A pod can only be scheduled on a tainted node if it carries a matching toleration.

A taint consists of three parts — key, value, and effect — formatted as key=value:effect. The key and value are a freeform pair (similar to a label assignment) that identify what kind of taint this is. The effect tells the scheduler how to enforce it.

Primary use case: Protect dedicated nodes (GPU nodes, control plane nodes, spot-instance pools) from general workloads.

Taints and Tolerations
EffectSchedulingRunning pods
NoScheduleBlocks new pods without a matching tolerationAlready-running pods continue unaffected
PreferNoScheduleScheduler avoids the node; will use it as last resortAlready-running pods continue unaffected
NoExecuteBlocks new pods and evicts already-running pods that don’t tolerate itPods are evicted immediately (or after tolerationSeconds)
Taint Effects
Terminal window
# Apply a taint — format: key=value:effect
kubectl taint node worker-node-02 special=true:NoSchedule
# Inspect taints on a node
kubectl get node worker-node-02 -o yaml | grep -A 5 taints:
# Remove a taint (append - to the taint string)
kubectl taint node worker-node-02 special=true:NoSchedule-
spec:
tolerations:
# Equal operator — key, value, and effect must all match the taint
- key: "special"
operator: "Equal"
value: "true"
effect: "NoSchedule"
# Exists operator — matches any taint with this key regardless of value
# - key: "special"
# operator: "Exists"
# effect: "NoSchedule"
# Wildcard — tolerates ALL taints on the node (use for infrastructure daemons)
# - operator: "Exists"
containers:
- name: app
image: nginx:1.27.1

A toleration matches a taint when key + effect are identical (and value matches if operator: Equal).

Toleration operatorvalue field required?Matches
EqualYesTaint with same key, value, and effect
ExistsNoAny taint with the same key (if effect specified) or any taint at all (if effect omitted)

tolerationSeconds — Delayed Eviction under NoExecute

Section titled “tolerationSeconds — Delayed Eviction under NoExecute”

With NoExecute, you can specify how long a pod should remain on the node after the taint appears before being evicted. This is critical for handling transient node conditions gracefully:

tolerations:
- key: "node.kubernetes.io/not-ready"
operator: "Exists"
effect: "NoExecute"
tolerationSeconds: 300 # stay running for 5 minutes; evict if node is still not-ready

Without tolerationSeconds, a NoExecute toleration lets the pod stay indefinitely.

Kubernetes automatically applies taints in response to node conditions. Knowing these is essential for designing pods that survive node failures:

TaintWhen applied
node.kubernetes.io/not-ready:NoExecuteNode fails its readiness check
node.kubernetes.io/unreachable:NoExecuteAPI server cannot reach the node
node.kubernetes.io/memory-pressure:NoSchedulekubelet reports low memory
node.kubernetes.io/disk-pressure:NoSchedulekubelet reports low disk
node.kubernetes.io/pid-pressure:NoSchedulekubelet reports PID pressure
node.kubernetes.io/unschedulable:NoScheduleNode is cordoned (kubectl cordon)
node-role.kubernetes.io/control-plane:NoScheduleDefault on all control plane nodes

Controls how pods are distributed across topology domains (zones, nodes, racks) for resilience and availability. Unlike affinity, which attracts pods to specific nodes, topology spread constraints enforce even distribution.

spec:
topologySpreadConstraints:
- maxSkew: 1 # max pod count difference between any two zones
topologyKey: topology.kubernetes.io/zone # domain: standard zone node label
whenUnsatisfiable: DoNotSchedule # hard: keep pod Pending if constraint can't be met
labelSelector:
matchLabels:
app: web # count only pods with this label
containers:
- name: app
image: nginx:1.27.1
Pod Topology Spread Constraints
FieldPurpose
maxSkewMaximum allowed difference in pod count between the most and least loaded topology domain
topologyKeyNode label key that defines the domain boundaries (e.g. topology.kubernetes.io/zone → zone-level spreading, kubernetes.io/hostname → node-level spreading)
whenUnsatisfiableDoNotSchedule (hard — pod stays Pending) or ScheduleAnyway (soft — schedule but try to minimize skew)
labelSelectorIdentifies which pods to count when evaluating skew

Practical Example: 6 Replicas Across 3 Zones

Section titled “Practical Example: 6 Replicas Across 3 Zones”
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 6
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
topologySpreadConstraints:
# Spread evenly across zones (max 1 pod difference per zone)
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: web
# Also spread across individual nodes within each zone
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway # soft — don't block if a node is full
labelSelector:
matchLabels:
app: web
containers:
- name: app
image: nginx:1.27.1

Kubernetes uses PriorityClasses to determine scheduling order when the cluster is under resource pressure. Higher-priority pods schedule first and can preempt (evict) lower-priority pods to make room.

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: high-priority
value: 1000000 # higher value = higher priority (default pods have value 0)
globalDefault: false # if true, applied to all pods without a priorityClassName
description: "For critical production services"
Terminal window
kubectl get priorityclasses
# NAME VALUE GLOBAL-DEFAULT
# high-priority 1000000 false
# system-cluster-critical 2000000000 false
# system-node-critical 2000001000 false
spec:
priorityClassName: high-priority
containers:
- name: app
image: nginx:1.27.1

If a high-priority pod cannot be scheduled due to insufficient resources, the scheduler will:

  1. Find a node where evicting lower-priority pods would free enough capacity
  2. Gracefully terminate those lower-priority pods (honouring terminationGracePeriodSeconds)
  3. Schedule the high-priority pod on the now-available node
ClassValueUsed by
system-node-critical2,000,001,000Node-essential pods (kube-proxy, CSI drivers)
system-cluster-critical2,000,000,000Cluster-essential pods (coredns, CNI plugins)

MechanismDirectionHard or SoftPrimary use case
nodeNameAssigns to specific nodeHardDebugging only — bypasses all scheduler logic
nodeSelectorAttracts to labeled nodesHardSimple node targeting (exact label match)
Node Affinity (required)Attracts to nodes matching expressionsHardComplex targeting with operators (In, Gt, etc.)
Node Affinity (preferred)Attracts to preferred nodesSoft”Prefer zone A but don’t block”
Node Anti-AffinityRepels from nodes matching expressionsHard or SoftWorkload isolation, zone spreading
Pod AffinityCo-locates with matching podsHard or SoftPerformance — keep app and cache on same node
Pod Anti-AffinitySeparates from matching podsHard or SoftHA — spread replicas across nodes/zones
Taint + NoScheduleRepels from tainted nodesHardDedicated nodes (GPU, spot, control plane)
Taint + PreferNoScheduleSoft repel from tainted nodesSoftGentle node isolation
Taint + NoExecuteRepels + evictsHardNode draining, emergency isolation
Topology SpreadDistributes evenly across domainsHard or SoftHigh availability across zones/nodes
Priority / PreemptionSchedules high-priority pods firstHardCritical workloads that must schedule regardless