Node Selector

spec.nodeSelector is the simplest pod-to-node placement constraint in Kubernetes: a flat map of label key/value pairs that the scheduler treats as a hard requirement — the Pod is feasible only on nodes that carry every one of those labels with exactly those string values (Assigning Pods to Nodes). It is the oldest and least expressive of the placement mechanisms: equality-only (no “in a set”, no “exists”, no “greater than”) and AND-only (every pair must match; there is no OR). For anything beyond “this Pod needs an SSD node” it has been functionally superseded by Node Affinity, which expresses the same idea plus set membership, soft preferences, and disjunction. nodeSelector survives because for the trivial case it is unbeatably terse — and because both can coexist on one Pod, in which case both must be satisfied (they are ANDed, never treated as alternatives).

Mental Model

nodeSelector is a set-membership filter expressed as a label dictionary. Picture every node carrying a bag of key=value labels; the Pod’s nodeSelector is a smaller bag; the node is a candidate iff the Pod’s bag is a subset of the node’s bag. There is no scoring, no preference, no fallback — a node either has all the labels or it does not.

flowchart TD
    POD["Pod<br/>nodeSelector:<br/>disktype: ssd<br/>gpu: 'true'"] --> SCHED[kube-scheduler<br/>NodeAffinity Filter plugin]
    N1["Node A<br/>disktype=ssd<br/>gpu=true"] -->|subset match| SCHED
    N2["Node B<br/>disktype=ssd"] -->|missing 'gpu' key| SCHED
    N3["Node C<br/>disktype=hdd<br/>gpu=true"] -->|wrong 'disktype' value| SCHED
    SCHED -->|feasible| N1
    SCHED -.->|rejected| N2
    SCHED -.->|rejected| N3

What this diagram shows and the insight to extract. Three nodes, one Pod requiring disktype=ssd AND gpu=true. Only Node A’s label bag is a superset of the Pod’s request. Node B is rejected for a missing key; Node C for a wrong value. The insight: nodeSelector is pure conjunction over exact equality — there is no way to say “ssd OR nvme” or “any node with a gpu label regardless of value.” The instant you need that, you have outgrown nodeSelector and want Node Affinity. Note also that the constraint is evaluated by the same NodeAffinity Filter plugin that handles node affinity — nodeSelector is, mechanically, a degenerate special case of node affinity inside the scheduler, and they share a code path and the same FailedScheduling event text.

Mechanical Walk-through

A node carries labels two ways: automatic well-known labels the kubelet and cloud-controller-manager populate, and operator-applied labels set with kubectl label nodes. When a Pod with a nodeSelector enters the scheduler, the NodeAffinity Filter plugin (see Scheduling Framework) reads the Pod’s spec.nodeSelector map and, for each candidate node, checks that every key is present with a matching value. A single mismatch rejects the node — it never reaches the Score phase. If no node matches, the Pod stays Pending with a FailedScheduling event of the form 0/N nodes are available: N node(s) didn't match Pod's node affinity/selector.

The crucial well-known node labels usable as nodeSelector keys (Well-Known Labels reference):

  • kubernetes.io/hostname — the node’s name; pin a Pod to one specific node (though spec.nodeName is the blunter tool for a literal pin).
  • kubernetes.io/oslinux or windows; the canonical guard for an OS-specific workload. A Linux container scheduled onto a Windows node simply fails to start, so every workload in a mixed-OS cluster should set this.
  • kubernetes.io/archamd64, arm64, etc.; pin a single-arch image to a compatible CPU architecture in a mixed-arch (e.g. AWS Graviton arm64 alongside x86) cluster.
  • topology.kubernetes.io/zone — the cloud availability zone (e.g. us-east-1a).
  • topology.kubernetes.io/region — the cloud region (e.g. us-east-1).
  • node.kubernetes.io/instance-type — the cloud instance type (e.g. m6i.4xlarge); steer a memory-hungry Pod to large instances.
  • node.kubernetes.io/windows-build — the Windows OS build, on Windows nodes only; matters because Windows containers require host/container build parity.

The legacy beta.kubernetes.io/arch, beta.kubernetes.io/os, beta.kubernetes.io/instance-type, and failure-domain.beta.kubernetes.io/{zone,region} labels are deprecated in favor of the non-beta forms above; older manifests still reference them but new work should not.

Operator-applied labels are arbitrary: disktype=ssd, gpu=true, team=payments. A label is set with kubectl label nodes worker-1 disktype=ssd and removed with the trailing-dash syntax disktype-.

nodeSelector and DaemonSets. A DaemonSet with a .spec.template.spec.nodeSelector runs its Pod only on matching nodes; with none, it runs on every node (DaemonSet docs). This is how a monitoring agent or CNI Pod is restricted to (say) Linux nodes. Internally the DaemonSet controller translates the eligible-node set into per-Pod node affinity pinning each Pod to one node, and the default scheduler then binds — so even DaemonSet placement flows through the same NodeAffinity plugin.

Scheduling-time only. nodeSelector is evaluated only at scheduling time. If a node’s label changes after the Pod is bound, the running Pod is not evicted — the same “ignored during execution” semantics that Node Affinity’s name makes explicit. There is deliberately no controller that watches node labels and evicts mismatched running Pods; when you need eviction-on-state-change, the tool is Taints and Tolerations with the NoExecute effect.

Configuration / API Surface

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ssd-cache
spec:
  replicas: 3
  selector:
    matchLabels:
      app: ssd-cache
  template:
    metadata:
      labels:
        app: ssd-cache
    spec:
      nodeSelector:                                   # the entire placement constraint
        disktype: ssd                                 # operator-applied label
        kubernetes.io/arch: amd64                     # well-known label — exclude arm64 nodes
        node.kubernetes.io/instance-type: m6i.2xlarge # pin to one instance type
      containers:
        - name: cache
          image: redis:7

Line-by-line. nodeSelector lives directly under the Pod spec (here inside the Deployment’s template.spec). Each map entry is an independent AND clause: the node must satisfy disktype=ssd and kubernetes.io/arch=amd64 and node.kubernetes.io/instance-type=m6i.2xlarge. There is no syntax for “or”, for “any value”, or for “not equal” — those require Node Affinity. The values are compared as strings; even a boolean-looking value such as gpu: "true" must be quoted, because unquoted YAML would parse true as a boolean and the label comparison would mismatch the stored string "true".

Apply a matching node label so the Deployment can schedule:

kubectl label nodes worker-3 disktype=ssd          # add the label
kubectl get nodes -l disktype=ssd                  # list nodes that match
kubectl get nodes --show-labels                    # see every label on every node
kubectl label nodes worker-3 disktype-             # remove it (trailing dash)

If nodeSelector and affinity.nodeAffinity are both set on a Pod, the scheduler requires both — a frequent surprise that produces a stricter constraint than the author intended.

Failure Modes

  • Typo in a label key or value. disktype: sdd (transposed) matches nothing; the Pod stays Pending forever with 0/N nodes are available: N node(s) didn't match Pod's node affinity/selector. Diagnose by diffing kubectl get nodes --show-labels against the Pod’s spec.nodeSelector from kubectl get pod -o yaml.
  • Label drift. A node loses a label (a re-image, a tooling bug, a cloud-controller change) and new Pods can no longer schedule there, while existing Pods keep running — a confusing partial outage. The asymmetry (scheduled Pods unaffected, new ones stuck) is the tell.
  • Over-constraining into a single point of failure. A nodeSelector so specific it matches one node funnels every replica onto that node; the node dies and the whole Deployment goes down. nodeSelector expresses requirement, never spread — pair it with Topology Spread Constraints for HA.
  • Reserved-prefix labels rejected. Labels under kubernetes.io/ and k8s.io/ are reserved; a kubelet self-applying one (or one under node-restriction.kubernetes.io/) can be blocked by the NodeRestriction admission plugin. Use your own domain prefix (example.com/disktype) for custom labels.
  • nodeSelector plus nodeAffinity over-constraint. Both must hold; authors who think one overrides the other create an unschedulable Pod. Pick one mechanism per intent.

Alternatives and When to Choose Them

MechanismExpressivenessHard / SoftWhen to use
nodeSelectorequality + AND onlyhardtrivial “must have label X” cases
Node Affinityset ops (In/NotIn/Exists/Gt/Lt), AND + ORhard and softanything beyond exact equality; preferences
Taints and Tolerationsnode repels Pods by defaulthard, incl. NoExecute evictiondedicating nodes; the inverse — opt-out not opt-in
Topology Spread Constraintseven distribution across domainssoft or hardHA spreading, which nodeSelector cannot do
spec.nodeNameexact node, bypasses the scheduler entirelyhardstatic Pods, debugging — almost never in production

Honest “when nodeSelector is still fine.” Despite node affinity being a strict superset, nodeSelector remains the right tool when all of: the constraint is one-to-three exact-match labels; it is a hard requirement (no preference, no fallback); and the manifest is read by humans who benefit from the terseness. Examples: kubernetes.io/os: linux on a Linux-only DaemonSet, or a single disktype: ssd. The verbosity of nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[].matchExpressions[] to express the identical thing is not worth it. The moment you want “in a set”, “exists”, a soft preference, or disjunction, switch to Node Affinity — it is the official recommendation and a clean superset.

Production Notes

  • Mixed-architecture clusters (AWS Graviton arm64 alongside x86 amd64) are the most common modern nodeSelector use: kubernetes.io/arch pins a single-arch image to compatible nodes. Multi-arch container images make even this unnecessary, but nodeSelector is the fallback for images built for only one architecture.
  • Windows + Linux clusters universally set kubernetes.io/os on every workload; omitting it lets a Linux container be scheduled onto a Windows node, where it fails to start with an opaque runtime error.
  • Managed node pools. GKE, EKS, and AKS each expose their pool identity as a node label — cloud.google.com/gke-nodepool on GKE, eks.amazonaws.com/nodegroup on EKS, agentpool (or kubernetes.azure.com/agentpool) on AKS — and steering Pods to a specific pool via nodeSelector is the standard pattern. Node affinity is increasingly preferred for the same purpose because its preferred form degrades gracefully when a pool is full, whereas nodeSelector simply leaves the Pod Pending.
  • Anti-pattern: encoding HA into nodeSelector. Teams sometimes expect a nodeSelector to spread replicas and are surprised when all replicas pile onto one matching node. nodeSelector is a placement filter, never a spreader; HA requires Topology Spread Constraints or Pod Affinity and Anti-Affinity.
  • Security-relevant labels should use the node-restriction.kubernetes.io/ prefix so a compromised kubelet cannot relabel its own node into a sensitive workload’s feasible set — relevant when nodeSelector gates placement of privileged workloads.

See Also