Pod affinity vs node affinity: what each rule decides

What each rule matches, when it’s hard or soft, and how a strict rule keeps a node, and its cost, in the cluster.

Published 1 November 2026 · By Harsh, Guest author · Network engineer at Palo Alto Networks · Published by Zoorik

Pod affinity vs node affinity: pod affinity reads running pods, node affinity reads node labels

Short answer. Pod affinity vs node affinity comes down to what each rule reads: the node’s own labels, or the pods running in a topology domain. Node affinity picks nodes by label, and pod affinity and anti-affinity place a pod near other pods or away from them.

The difference between pod affinity and node affinity is what the rule reads

Picture a web Deployment with 3 replicas that must stay in one node pool on Azure Kubernetes Service (AKS). No two replicas may share a node.

Staying in the pool depends only on the node’s own label, so it’s node affinity. Keeping replicas apart depends on which pods already run on each node, so it’s pod anti-affinity. Make that rule required, and the pool needs 3 nodes whatever the replicas request.

Node affinity sits in .spec.affinity.nodeAffinity and reads the node’s own labels. Those can be topology.kubernetes.io/zone, a node pool label your cloud adds, or one you add yourself.

Inter-pod affinity sits in .spec.affinity.podAffinity and .spec.affinity.podAntiAffinity. It reads the labels of pods already running in a topology domain.

The domain is set by topologyKey, which names a node label: with kubernetes.io/hostname each node is its own domain. With topology.kubernetes.io/zone, each zone is one domain.

Node affinity reads the node, pod anti-affinity reads its pods On the left, the highlighted subject, web replica 3, with two required rules: node affinity to pool A, and anti-affinity that allows no other web replica on the node. An arrow labelled filter forks into four node rows, each showing the node's labels and its pods. node-1: label pool A, one web pod, verdict no: a web replica runs here, read from its pods. node-2: label pool A, one web pod, verdict no: a web replica runs here, read from its pods. node-3: label pool A, no pods, a green check, fits. node-4: label pool B, no pods, verdict no: label doesn't match, read from its labels. The pool has 3 nodes for 3 replicas. Node affinity reads the node, pod anti-affinity reads its pods labelspodsnode-1pool: Awebno: a web replica runs hereread from its podsnode-2pool: Awebno: a web replica runs hereread from its podsnode-3pool: Afitsnode-4pool: Bno: label doesn't matchread from its labelsweb replica 3node affinity, required:pool Aanti-affinity, required:no other web replicaon the nodefilter
Illustration of the opening case: pool A has 3 nodes for 3 replicas, and the domain is one node.

Required rules filter nodes, preferred rules score them

Scheduling a pod takes two passes. First the scheduler filters out every node where the pod can’t run. Then it scores the nodes that are left and picks the highest.

Each kind of affinity comes in two strengths, one for each pass:

  • Required. A requiredDuringSchedulingIgnoredDuringExecution rule is a hard filter. A node that breaks it is never a candidate.
  • Preferred. A preferredDuringSchedulingIgnoredDuringExecution rule carries a weight from 1 to 100, which is added to the score of every node that meets it. For anti-affinity, the weight is subtracted from every node whose domain already runs a matching pod. When no node meets a preferred rule, the pod still lands on one that passes the filters.

Rules are checked only when a pod is scheduled, which is what IgnoredDuringExecution in each name means. The Karpenter docs note that Kubernetes never implemented any other variant. A rule applies again, in full, only when a pod is placed: on a rollout, a scale-up or a node drain.

Node affinity vs node selector: exact labels, or operators and preferences

Use nodeSelector when one exact label match says everything. Use node affinity when you need an OR, a NotIn, a numeric comparison or a preference.

nodeSelector is the simplest constraint: a map of labels. Kubernetes schedules the pod only onto nodes that carry every one of them.

Node affinity covers the same ground and adds three things:

  • operators: In, NotIn, Exists, DoesNotExist, Gt and Lt
  • OR logic: terms in nodeSelectorTerms are ORed, and the expressions inside one term are ANDed
  • preferred rules, which only shift the score

If a pod sets both nodeSelector and node affinity, a node has to satisfy both.

List your nodes’ labels first:

kubectl get nodes --show-labels

This node affinity keeps a pod in one AKS node pool and prefers one zone inside it:

spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: kubernetes.azure.com/agentpool
            operator: In
            values:
            - <pool-name>
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 50
        preference:
          matchExpressions:
          - key: topology.kubernetes.io/zone
            operator: In
            values:
            - <zone>

So the scheduler drops every node outside the pool, then adds 50 to the score of each remaining node in that zone.

The pool rule removes nodes, the zone rule only ranks the rest Three stages read top to bottom, with three nodes in columns. Candidates: node-a, other pool, node-b, the pool, other zone, node-c, the pool, preferred zone. An arrow labelled filter, required pool rule, leads to Pass the filter: node-b and node-c pass, and node-a is struck through and marked never a candidate. An arrow labelled score, preferred zone rule, weight 50, leads to Score: node-b gets plus 0, and node-c, highlighted, gets plus 50 and a green check, placed. A dashed band reads: Without node-c, the pod still lands on node-b. The pool rule removes nodes, the zone rule only ranks the rest CandidatesPass the filterScorefilterrequired pool rulescorepreferred zone rule,weight 50node-aother poolnode-bthe pool, other zonenode-cthe pool, preferred zonenode-anever a candidatenode-bnode-cnode-b+0node-c+50placedWithout node-c, the pod still lands on node-b
Illustration of the node affinity above, with every other score equal.

kubernetes.azure.com/agentpool is an AKS reserved label that holds the node pool’s name. On Amazon Elastic Kubernetes Service (EKS), managed node groups carry labels prefixed eks.amazonaws.com. Among them is eks.amazonaws.com/capacityType, with the value ON_DEMAND or SPOT.

The EKS docs suggest that label for keeping stateful or fault-intolerant apps on On-Demand nodes. Fault-tolerant ones can go on Spot.

Node anti-affinity and taints

NotIn and DoesNotExist give you node anti-affinity, which keeps a pod off nodes with a given label. Taints work from the node’s side.

The docs call node affinity a property of pods that attracts them to nodes. Taints are “the opposite”: a tainted node repels pods that don’t tolerate the taint.

A toleration lets a pod onto a tainted node but doesn’t send it there. So dedicating a pool to one workload takes both halves. The taint keeps other pods out, and node affinity keeps that workload in.

Pod affinity and anti-affinity place a pod next to, or away from, other pods

Kubernetes pod affinity pairs a label selector over pods with a topologyKey. In the docs’ own example, each web replica requires a node (topologyKey: kubernetes.io/hostname) that already runs a cache pod labelled app=store.

Pod anti-affinity flips the test, so the pod avoids domains that already run matching pods. Its common job is spreading a Deployment’s replicas so that one node failure can’t take them all.

A preferred anti-affinity rule on hostname asks the scheduler to spread replicas when it can. It lets two share a node when it must:

# in the Deployment's pod template (spec.template.spec)
affinity:
  podAntiAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
    - weight: 100
      podAffinityTerm:
        labelSelector:
          matchExpressions:
          - key: app
            operator: In
            values:
            - web
        topologyKey: kubernetes.io/hostname

matchLabelKeys, stable since Kubernetes 1.33, adds the incoming pod’s own label values to the selector. With pod-template-hash, it counts only pods from the same Deployment revision, so a rolling update doesn’t break the rule.

Only In, NotIn, Exists and DoesNotExist work here. Gt and Lt are for node affinity only, and topologyKey can’t be empty.

Inter-pod rules cost scheduling time. The docs say they “require substantial amounts of processing” and don’t recommend them in clusters larger than several hundred nodes.

The Cluster Autoscaler FAQ adds that its latency targets hold only if no pod uses pod affinity or anti-affinity. Anti-affinity also needs every node to carry the topologyKey label.

Pod anti-affinity vs node affinity

Node affinity asks what the node is, and pod anti-affinity asks what already runs there. “Not on Spot nodes” is node affinity with NotIn. “Not on a node that already runs another replica” is pod anti-affinity.

Sometimes “what runs there” really means “nodes of one kind”. Then label those nodes and use node affinity, which stays true as pods come and go.

Pod affinity vs node affinity: node labels per node, or pods per domain

nodeSelectorNode affinityPod affinityPod anti-affinity
Matches
Node labels
Node labels
Labels of pods already in the domain
Labels of pods already in the domain
Field
spec.nodeSelector
spec.affinity.nodeAffinity
spec.affinity.podAffinity
spec.affinity.podAntiAffinity
Judged per
Node
Node
Domain named by topologyKey (node, zone)
Domain named by topologyKey (node, zone)
Hard rule
Always hard
required…
required…
required…
Soft rule
None
preferred…, weight 1 to 100 added
preferred…, weight 1 to 100 added
preferred…, weight 1 to 100 subtracted
Operators
Exact match on every label
In, NotIn, Exists, DoesNotExist, Gt, Lt
In, NotIn, Exists, DoesNotExist
In, NotIn, Exists, DoesNotExist
Large clusters
No warning in the docs
No warning in the docs
Not recommended past several hundred nodes
Not recommended past several hundred nodes
Typical use
One fixed label
Pool, zone, capacity type, hardware
Keep chatty services together
Spread replicas
How it can hold a node
Pods can move only to labelled nodes
Same, for required terms
Pods can move only to where their partner runs
Required hostname rule needs one node per replica

A required rule can keep a node, and its cost, in the cluster

Draining a node evicts its pods. Their controllers create replacements, and the scheduler places each one under the same rules as before. So a replacement with nowhere to go stays Pending.

The Cluster Autoscaler checks for that before it removes a node. Its FAQ says it simulates the kube-scheduler: “if there’s no other node where a given pod can schedule, the pod’s node won’t be scaled down.”

Required anti-affinity holds one node per replica

Required anti-affinity on kubernetes.io/hostname is the plain case: a Deployment with 3 replicas runs one per node, so it needs 3 nodes. That holds whatever those replicas request.

Suppose they can run only in a 3-node pool. Every node in it could be close to idle, and still none could go.

The FAQ names this pattern. Pods that fit one per node on some subset of nodes “will always block CA from scaling down their nodes”.

The replacement would stay Pending, so the Cluster Autoscaler keeps node-3, and the same check keeps node-1 and node-2. The floor is the replica count, so 4 replicas would need 4 nodes.

3 replicas with required anti-affinity hold all 3 nodes Three node boxes side by side, node-1, node-2, node-3, each running one web replica. node-3 is highlighted, with remove node-3? above it. An arrow labelled drain leads from node-3 to a dashed chip, web, replacement. Dashed lines from it reach node-1 and node-2, each ending in a cross, with one label: another web replica here. Below: 0 of 2 nodes pass: the replacement stays Pending, so node-3 stays. 3 replicas with required anti-affinity hold all 3 nodes node-1webnode-2webnode-3webremove node-3?drainweb, replacementanother web replica here0 of 2 nodes pass: the replacement stays Pending, so node-3 stays
Illustration of the worked example: required anti-affinity on hostname in a pool of 3 nodes, and the same check keeps node-1 and node-2.

The docs’ web-and-cache example locks the same way. Both Deployments carry required hostname anti-affinity, and the web tier also requires a cache pod on its node. So in the docs’ three-node cluster, each node ends up with one pair that can’t move.

Other required rules narrow where a pod can go

These don’t pin a pod to one node, but each one narrows its choices:

  • One pool. Required node affinity to one pool means the pod can move only inside that pool. If the pool’s other nodes are full, its node stays.
  • One per zone. Required anti-affinity on topology.kubernetes.io/zone allows one matching pod per zone. So a fourth replica in a three-zone cluster stays Pending.
  • Volumes. A volume can carry node affinity too. Topology-aware provisioners set it on the PersistentVolumes they create. That ties the pods using that volume to nodes in its zone.

Preferred rules depend on the autoscaler

The Cluster Autoscaler respects nodeSelector and required node affinity when it picks a node group to grow. That works if the groups are labelled to match. Preferred rules play no part in that choice.

When Karpenter builds a pod’s requirements, it starts by treating preferred affinities as required. It relaxes them one at a time, only when they can’t be met.

Karpenter also uses them to judge whether a pod can shift to another node. Its docs warn that preferred affinities “can result in more nodes being created than expected”.

Choosing the rule: preferred by default, required only where the app needs it

Default to preferred, and make a rule required only where breaking it breaks the app.

  • If the pod needs something only some nodes have, such as a zone, a pool, a GPU or On-Demand capacity, use required node affinity. For one exact label, nodeSelector is enough.
  • If the pod runs better on some nodes but fine on any, use preferred node affinity.
  • If other pods must stay off a pool, taint the pool and give the toleration to the pods that belong there. Then add node affinity to those pods.
  • If two services talk a lot, use preferred pod affinity on hostname or zone. Make it required only if the app can’t work across zones.
  • If replicas should sit apart, use preferred pod anti-affinity on hostname or a topology spread constraint. That page compares the two.
  • If two replicas on one node would be unsafe, use required hostname anti-affinity. ZooKeeper, a quorum store, is the docs’ high-availability example. Count one node per replica, plus a spare so a drain has somewhere to put the evicted one.

Within the room these rules leave, requests decide how many nodes the pods fill. How bin packing works covers that half. The Kubernetes cost optimization guide takes requests first, then nodes.

Cluster Autoscaler not scaling down lists the other reasons a node stays.

How Coral checks affinity before it drains a node

Coral optimizes Kubernetes on AKS and EKS. It rebalances CPU and memory requests and drains spare nodes so your autoscaler can remove them. When it consolidates nodes, its drain follows the same scheduling rules:

  • The check. Before it drains a spare node, Coral simulates the scheduler on the live cluster. Every pod on the node must fit elsewhere, with its affinity, taints, spread, ports and zones. If one pod has nowhere to go, the node isn’t drained.
  • The limits. It respects PodDisruptionBudgets, do-not-disrupt and your autoscaler’s minimum pool size. Every pool keeps at least one node.
  • The drain. It cordons and drains one node at a time. If a drain can’t finish, the node goes back into service.

Where requests sit above real use, rebalancing them leaves more room on the nodes that stay. So more pods fit elsewhere, and more nodes can go. A required one-per-node rule still keeps its nodes, because that floor comes from the rule and not from requests.

The Coral FAQ and the guide’s consolidation section have the detail.

Sources: Kubernetes, “Assigning Pods to Nodes” (opens in a new tab); Kubernetes, “Taints and Tolerations” (opens in a new tab); Kubernetes, “Pod” (opens in a new tab) (API reference: NodeAffinity, WeightedPodAffinityTerm); Kubernetes, “Well-Known Labels, Annotations and Taints” (opens in a new tab); Kubernetes autoscaler, “Cluster Autoscaler FAQ” (opens in a new tab); Karpenter, “Scheduling” (opens in a new tab); Microsoft Learn, “Use labels in an Azure Kubernetes Service (AKS) cluster” (opens in a new tab); AWS, “Simplify node lifecycle with managed node groups” (opens in a new tab).

Find your own number

One line in your cloud console. One PDF report with your estimated saving at list price.

Estimate your saving

Talk to the builders

Thirty minutes with the people who build Coral.

Book a demo (opens in a new tab)

Or write to founders@zoorik.com