A three-replica Deployment guarantees nothing about how those replicas are distributed: the scheduler can perfectly well place all three pods on the same node if nothing explicitly prevents it. Losing that single node then takes down all three replicas at once, exactly the scenario a PodDisruptionBudget is supposed to protect against, except no PDB can do anything once the initial placement already concentrated everything in one place.
Pod anti-affinity: spreading by relative exclusion
Anti-affinity doesn’t tell a pod where to go in absolute terms, it tells it to avoid nodes already running other pods carrying a specific label:
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: payment-api
topologyKey: "kubernetes.io/hostname"
requiredDuringSchedulingIgnoredDuringExecution is a hard constraint: the scheduler refuses to place the pod if no node satisfies the rule, leaving it Pending rather than compromise. The preferred variant softens the constraint: the scheduler tries to honor the spread without blocking the deployment if the cluster doesn’t have enough available nodes to achieve it perfectly. The field’s misleading name is worth reading slowly: “ignored during execution” means the rule only applies at placement time, never afterward. An already-running pod is never evicted if the cluster’s topology later changes and violates the rule.
Topology spread constraints: a measured spread, not binary
Anti-affinity answers a binary question (does this node already have this label, yes or no). Topology spread constraints answer a measurement question: what’s the maximum tolerated gap between the pod count on each zone or node.
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: "topology.kubernetes.io/zone"
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: payment-api
maxSkew: 1 means no zone can host more than one pod more than the least-loaded zone: with three zones and three replicas, one pod per zone satisfies the constraint, any bigger imbalance violates it. whenUnsatisfiable determines what happens if exact balance becomes impossible: DoNotSchedule blocks like a hard constraint, ScheduleAnyway degrades gracefully to a preference, the same distinction as required/preferred for anti-affinity, with different vocabulary.
Which to choose: the question being asked, not tool preference
Anti-affinity fits well for “never put these two specific pods together” (two instances of the same database that must never share a node, for instance). Topology spread constraints fit better for “spread N replicas as evenly as possible across zones or nodes,” a balancing problem rather than an exclusion one. The two mechanisms combine without conflict on the same Deployment when the real need spans both questions.
The link to the cluster autoscaler
An overly strict anti-affinity constraint can block the cluster autoscaler’s scale down: if evicting a pod from a node means rescheduling it onto a node that would violate its own anti-affinity rule, the autoscaler refuses the removal rather than violate a constraint set elsewhere. A node that looks underused but stays stuck refusing removal often hides exactly this kind of placement constraint, not an autoscaler bug.
Takeaway
Three replicas with no spread constraint can land on the same node, something no PodDisruptionBudget catches after the fact. Anti-affinity excludes by label in a binary way; topology spread constraints measure a tolerable imbalance. The two combine, and both can block a cluster autoscaler scale down if set too strictly. Getting this spread right from the start is part of what actually protects the high availability multiple replicas are supposed to promise.