Topology Spread Constraints (TSC) are the modern, recommended way to distribute Pods evenly across failure domains — zones, nodes, or any custom topology. They're more flexible and more efficient than Pod anti-affinity for spreading workloads.

1. The Core Idea: maxSkew

A Topology Spread Constraint says: "The difference in Pod count between any two topology domains must not exceed maxSkew."

maxSkew=1 across zones (6 replicas, 3 zones) Zone A Pod Pod Count: 2 Zone B Pod Pod Count: 2 Zone C Pod Pod Count: 2 ✓ Skew = max(2,2,2) - min(2,2,2) = 0 ≤ 1
# The formula:
# skew = (max count in any domain) - (min count in any domain)
# Constraint: skew ≤ maxSkew

# maxSkew=1: perfectly balanced (difference of at most 1)
# maxSkew=2: allows some imbalance (difference of at most 2)
# maxSkew=3: very relaxed (rarely useful)

Basic Spec

spec:
  topologySpreadConstraints:
    - maxSkew: 1                              # Max allowed imbalance
      topologyKey: topology.kubernetes.io/zone # What defines a "domain"
      whenUnsatisfiable: DoNotSchedule        # Hard or soft?
      labelSelector:                          # Which Pods to count
        matchLabels:
          app: web

Fields Explained

FieldPurposeValues
maxSkewMaximum allowed difference between domainsInteger ≥ 1
topologyKeyNode label defining topology domainszone, hostname, custom
whenUnsatisfiableWhat to do if constraint can't be metDoNotSchedule (hard) / ScheduleAnyway (soft)
labelSelectorWhich Pods are counted for skew calculationLabel selector (like Services)
minDomainsMinimum number of domains to consider (K8s 1.25+)Integer (prevents over-concentration when few domains exist)
nodeAffinityPolicyWhether to honor node affinity when counting (1.26+)Honor / Ignore
nodeTaintsPolicyWhether to honor taints when counting (1.26+)Honor / Ignore
labelSelector counts ALL matching Pods in the cluster (not just those from this Deployment). If two Deployments both have app: web Pods, the constraint counts Pods from both. Use unique labels if you want per-Deployment spreading.

2. whenUnsatisfiable — Hard vs Soft

ValueBehaviorUse Case
DoNotSchedulePod stays Pending if placing it would violate maxSkewCritical HA (must be spread)
ScheduleAnywayScheduler tries to minimize skew but doesn't block schedulingGeneral spreading (best-effort)
# Hard: 7 replicas, 3 zones, maxSkew=1
# Zone A: 3, Zone B: 2, Zone C: 2 → skew = 3-2 = 1 ✓
# 8th Pod: would make A:4, skew=4-2=2 → BLOCKED (DoNotSchedule)
#          must go to B or C: A:3, B:3, C:2 → skew=1 ✓

# Soft (ScheduleAnyway): same scenario
# 8th Pod: scheduler prefers B or C, but if they're full/unavailable,
# it CAN go to A (skew=2, which exceeds maxSkew, but Pod still schedules)
Use DoNotSchedule for stateful workloads where zone spread is a hard requirement (databases, consensus systems). Use ScheduleAnyway for stateless services where spreading is desired but availability is more important than perfect balance. You don't want a web server stuck Pending because of a skew violation.

3. Multiple Constraints (Zone + Node Spreading)

The most common production pattern: spread across zones AND across nodes within each zone:

spec:
  topologySpreadConstraints:
    # Spread across zones (hard):
    - maxSkew: 1
      topologyKey: topology.kubernetes.io/zone
      whenUnsatisfiable: DoNotSchedule
      labelSelector:
        matchLabels:
          app: web
    # Also spread across nodes within each zone (soft):
    - maxSkew: 1
      topologyKey: kubernetes.io/hostname
      whenUnsatisfiable: ScheduleAnyway
      labelSelector:
        matchLabels:
          app: web
Result: 6 replicas across 3 zones, spread within zones Zone A (2 Pods) Node 1: Pod Node 2: Pod ↑ Spread across nodes too Zone B (2 Pods) Node 3: Pod Node 4: Pod Zone C (2 Pods) Node 5: Pod Node 6: Pod
Multiple constraints are AND'd together. The scheduler must satisfy ALL constraints simultaneously. Zone spread (hard) ensures zone-level HA. Node spread (soft) optimizes within zones but doesn't block scheduling. This layered approach gives you maximum resilience.

4. Practical Patterns

Pattern: Cluster-Wide Default (K8s 1.24+)

Set default topology spread constraints for ALL Pods in the cluster via scheduler configuration:

# In KubeSchedulerConfiguration:
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
  - pluginConfig:
      - name: PodTopologySpread
        args:
          defaultConstraints:
            - maxSkew: 3
              topologyKey: topology.kubernetes.io/zone
              whenUnsatisfiable: ScheduleAnyway
            - maxSkew: 5
              topologyKey: kubernetes.io/hostname
              whenUnsatisfiable: ScheduleAnyway
          defaultingType: List    # or "System" for K8s built-in defaults
Setting cluster-wide defaults means every Pod gets basic spreading without developers needing to add constraints to every Deployment. The defaults use ScheduleAnyway (soft) to avoid blocking Pods. Individual Deployments can override with tighter constraints when needed.

Pattern: Match Affinity and Spread Together

# Restrict to specific zones + spread evenly within those zones:
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions:
              - key: topology.kubernetes.io/zone
                operator: In
                values: [us-east-1a, us-east-1b]  # Only these 2 zones
  topologySpreadConstraints:
    - maxSkew: 1
      topologyKey: topology.kubernetes.io/zone
      whenUnsatisfiable: DoNotSchedule
      labelSelector:
        matchLabels:
          app: web
# Result: Pods only in zones a and b, evenly spread between them

Pattern: minDomains — Prevent Concentration

# Problem: If only 1 node exists with the right label, all Pods go there
# minDomains says "consider at least N domains" even if fewer exist now:
spec:
  topologySpreadConstraints:
    - maxSkew: 1
      topologyKey: topology.kubernetes.io/zone
      whenUnsatisfiable: DoNotSchedule
      minDomains: 3                        # Expect at least 3 zones
      labelSelector:
        matchLabels:
          app: web
# If only 2 zones exist → Pod stays Pending (waiting for 3rd zone)
# Prevents deploying all replicas in a degraded topology

Debugging Topology Spread

# Check current distribution:
kubectl get pods -l app=web -o custom-columns=\
NAME:.metadata.name,\
NODE:.spec.nodeName,\
ZONE:.metadata.labels.topology\.kubernetes\.io/zone

# Check scheduler events for spread violations:
kubectl describe pod web-pending
# Events:
#   Warning  FailedScheduling  doesn't satisfy spread constraint:
#   zone skew (3) > maxSkew (1)
CKA exam: Topology Spread Constraints are tested frequently. Remember the four required fields: maxSkew, topologyKey, whenUnsatisfiable, labelSelector. A common gotcha: the labelSelector must match the Pod's OWN labels (not some other Pod's). The constraint counts Pods with those labels to calculate skew.

Summary

ConceptKey Point
maxSkewMaximum allowed difference in Pod count between any two domains
topologyKeyNode label defining domains (zone, hostname, custom)
DoNotScheduleHard — Pod stays Pending if placement violates maxSkew
ScheduleAnywaySoft — scheduler minimizes skew but doesn't block
labelSelectorDefines which Pods are counted — must match own labels for self-spreading
Multiple constraintsAND'd together — spread across zones AND nodes simultaneously
minDomainsRequire minimum topology domains to exist before scheduling
vs Anti-AffinityTSC is more efficient, more flexible (tunable skew), recommended for spreading
Cluster defaultsSet in scheduler config — every Pod gets basic spreading automatically

📝 Quiz: Topology Spread Constraints

Q1: You have 5 Pods with maxSkew: 1 across 3 zones. The current distribution is Zone A: 2, Zone B: 2, Zone C: 1. Where can the 6th Pod go?

Only Zone C. Current skew: max(2,2,1) - min(2,2,1) = 2-1 = 1 (at the limit). Adding to A or B would make it 3-1=2 (exceeds maxSkew=1). Adding to C makes it 2-2=0. Only Zone C maintains the constraint.

Q2: What's the difference between DoNotSchedule and ScheduleAnyway?

DoNotSchedule: Hard constraint — if the Pod would violate maxSkew, it stays Pending. The Pod won't schedule even if there are available nodes.
ScheduleAnyway: Soft constraint — the scheduler tries to minimize skew (scores domains by imbalance) but will place the Pod even if it violates maxSkew. The Pod always schedules.

Q3: A Deployment uses labelSelector: {app: web} in its topology constraint. Another Deployment also has Pods labeled app: web. Are those counted?

Yes. The labelSelector counts ALL Pods matching those labels across the entire cluster (or namespace). It doesn't distinguish between Deployments. If you want per-Deployment spreading, use a more unique label (e.g., app: web, deployment: frontend-v2) in the labelSelector.

Q4: You have two topology spread constraints: zone (DoNotSchedule, maxSkew=1) and node (ScheduleAnyway, maxSkew=1). How are they evaluated?

They're AND'd together. The Pod must satisfy the hard zone constraint (no placement that violates zone skew). Among valid zones, the scheduler optimizes for node-level spread (soft — it tries but doesn't block). A Pod can only go to a zone/node combo that satisfies the hard constraint, with preference for balanced node distribution.

Q5: You set maxSkew: 1 with DoNotSchedule for zone spreading. You scale to 10 replicas but only 2 zones exist. What happens?

All 10 Pods schedule successfully. With 2 zones and maxSkew=1, the distribution will be 5-5 (skew=0). Since 10 is evenly divisible by 2, there's no problem. If it were 11 Pods: 6-5 distribution (skew=1, still within limit). TSC allows many Pods per domain — it just ensures the difference between domains is ≤ maxSkew.

Q6: When would you use minDomains: 3?

When you need to ensure a minimum number of topology domains exist before allowing scheduling. Without it, if only 1 zone is available (e.g., during a cloud outage), all Pods could land in that one zone — satisfying maxSkew (all in one domain = skew 0) but losing HA. With minDomains: 3, Pods stay Pending until 3 zones are available, preventing single-zone concentration.