Anti-affinity is the scheduler rule that keeps replicas apart. Where affinity says “run me next to these pods”, anti-affinity says “keep me away from them”, and the reason is almost always failure domains: if all three replicas land on one node, one node failure is a full outage.
The tension lives in a single word. required makes the rule a filter, so a pod with nowhere legal to go stays Pending forever; preferred makes it a score, so the same rule degrades gracefully. One YAML shape produces either a deadlock or a soft preference depending on which you typed.
A rule is a selector plus a topology
The rule has two parts. A labelSelector chooses which pods count as peers, and a topologyKey names the node label that defines a domain. kubernetes.io/hostname makes every node its own domain; topology.kubernetes.io/zone groups nodes into zones, so “keep apart” comes to mean “keep to different zones”. The namespaces field defaults to the pod’s own namespace, so anti-affinity never accidentally spans tenants.
Nodes that lack the topologyKey label are not part of any domain. The docs warn that anti-affinity requires consistently labelled nodes and that a missing key leads to unintended behaviour. In practice an unlabelled node is invisible to the rule: neither filtered out nor counted, so it can collect exactly the replicas the rule was meant to spread. One more wrinkle: preferred rules attached to pods that already exist are ignored when a new pod is scheduled. Only the incoming pod’s own rules count, so retrofitting a spread rule onto a running workload does not reshuffle what is already placed.
Required is a cap, not a guarantee
Required anti-affinity is a node filter, so it caps a workload at one replica per domain. Nine replicas across six nodes leaves three Pending forever, no matter how much spare capacity exists. Across three zones, four replicas leave one Pending. Every rejected pod reports FailedScheduling, the autoscaler keeps reporting that it cannot help, and nothing ever errors.
That is why the docs warn against required anti-affinity except where the hard constraint is real, such as one instance per node for a hostPort or a local SSD. They also note that inter-pod affinity and anti-affinity need substantial processing and are not recommended above several hundred nodes, which is a second reason to reach for something cheaper. A required rule is also not retroactive: it filters placement at admission time, so a node label that changes later does not evict pods already running, which is the IgnoredDuringExecution half of the name.
Spread constraints say the same thing more cheaply
topologySpreadConstraints expresses the same intent with an explicit tolerance. maxSkew is the permitted difference between the fullest domain and the global minimum; topologyKey names the domain; whenUnsatisfiable decides whether an unsatisfiable skew is fatal (DoNotSchedule, the default) or merely a scoring penalty (ScheduleAnyway). Nine replicas over three zones with maxSkew: 1 land as three, three, three.
The docs’ worked example is worth internalising: two zones holding two and one matching pods, with maxSkew: 1, forces the next pod into the emptier zone, because placing it in the fuller one makes the skew two. minDomains raises the global minimum so a cluster with fewer zones than expected cannot call itself balanced, and it is only valid with DoNotSchedule. The reason to prefer constraints over anti-affinity is cost as much as clarity: spread counts pods per domain, while anti-affinity evaluates a term against every node. Constraints also compose, so one per topology key can spread across zones and nodes at the same time.
When it breaks
Required anti-affinity with more replicas than domains. The workload scales up to the number of nodes and stops there, every further pod sits Pending, and the horizontal autoscaler keeps asking for more. The same trap appears in a small cluster: a two-node test cluster with three replicas and a required hostname rule can never run the third. Switch to preferred or to a spread constraint with ScheduleAnyway, and keep a hard rule only where one pod per node is genuinely required.
Nodes without the topology label. A node pool that never received topology.kubernetes.io/zone is outside every domain, so it is invisible to the constraint: replicas pile onto the labelled nodes while a third of the cluster sits unused. Verify with kubectl get nodes -L topology.kubernetes.io/zone that every node carries a value.
Check it quickly
Confirm the topology is real before trusting the spread:
kubectl get nodes -L topology.kubernetes.io/zone -L kubernetes.io/hostname
kubectl get pods -o custom-columns=NAME:.metadata.name,NODE:.spec.nodeName,PHASE:.status.phase
kubectl get events --field-selector reason=FailedScheduling --sort-by=.lastTimestamp | tail -5
kubectl get pods -o jsonpath='{range .items[*]}{.spec.nodeName}{"\n"}{end}' | sort | uniq -c
A blank cell in the first command means that node belongs to no domain. FailedScheduling events naming anti-affinity rules mean the required rule has no eligible node left. The last command shows the real distribution: a count of one per node means required anti-affinity is capping the workload, while uneven counts mean a spread constraint is not being honoured.
Sources
- Assigning Pods to Nodes, inter-pod affinity and anti-affinity rules, topology keys and scheduling behaviour, checked 2026-09-16.
- Pod Topology Spread Constraints, maxSkew, whenUnsatisfiable, minDomains and the worked skew example, checked 2026-09-16.