πŸ“šBook Signing at KubeCon EU 2026Meet us at Booking.com HQ (Mon 18:30-21:00) & vCluster booth #521 (Tue 24 Mar, 12:30-1:30pm) β€” free book giveaway!RSVP Booking.com Event
Deployments intermediate ⏱ 15 minutes K8s 1.28+

Topology Spread Constraints for HA Workloads

Distribute pods across nodes, zones, and regions using topology spread constraints. Ensure high availability and fault tolerance for your workloads.

By Luca Berton β€’ β€’ πŸ“– 5 min read

πŸ’‘ Quick Answer: Add topologySpreadConstraints to pod spec to distribute pods across zones/nodes. Key fields: topologyKey (label to spread across, e.g., topology.kubernetes.io/zone), maxSkew (max pod count difference), whenUnsatisfiable (DoNotSchedule or ScheduleAnyway).

Key example: maxSkew: 1, topologyKey: topology.kubernetes.io/zone ensures pods spread evenly across availability zones.

Gotcha: Constraints apply to new pods only; existing pods aren’t rebalanced. Use multiple constraints for zone AND node spread.

Topology spread constraints distribute pods evenly across failure domains like nodes, zones, or regions. Ensure high availability by preventing pod concentration.

Basic Node Spread

# spread-across-nodes.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
spec:
  replicas: 6
  selector:
    matchLabels:
      app: web-app
  template:
    metadata:
      labels:
        app: web-app
    spec:
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: web-app
      containers:
        - name: web
          image: nginx:1.25

Result with 6 pods on 3 nodes:

Node-1: 2 pods
Node-2: 2 pods  
Node-3: 2 pods

Zone-Aware Spread

# spread-across-zones.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
spec:
  replicas: 9
  selector:
    matchLabels:
      app: api-server
  template:
    metadata:
      labels:
        app: api-server
    spec:
      topologySpreadConstraints:
        # Spread across zones
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: api-server
        # Also spread within each zone across nodes
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels:
              app: api-server
      containers:
        - name: api
          image: api-server:v1

Result with 9 pods across 3 zones:

Zone-A: 3 pods (spread across nodes)
Zone-B: 3 pods (spread across nodes)
Zone-C: 3 pods (spread across nodes)

Understanding maxSkew

# maxSkew determines acceptable imbalance
topologySpreadConstraints:
  - maxSkew: 1  # Difference between most and least loaded domain
    topologyKey: kubernetes.io/hostname
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels:
        app: myapp

# maxSkew: 1 with 5 pods on 3 nodes:
# Valid:   Node-A: 2, Node-B: 2, Node-C: 1 (diff = 1) βœ“
# Invalid: Node-A: 3, Node-B: 2, Node-C: 0 (diff = 3) βœ—

whenUnsatisfiable Options

# DoNotSchedule - Hard constraint
topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: kubernetes.io/hostname
    whenUnsatisfiable: DoNotSchedule  # Pod stays Pending
    labelSelector:
      matchLabels:
        app: critical-app

# ScheduleAnyway - Soft constraint
topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: kubernetes.io/hostname
    whenUnsatisfiable: ScheduleAnyway  # Best effort, may exceed maxSkew
    labelSelector:
      matchLabels:
        app: flexible-app

Multiple Constraints

# multi-constraint-spread.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: distributed-app
spec:
  replicas: 12
  selector:
    matchLabels:
      app: distributed-app
  template:
    metadata:
      labels:
        app: distributed-app
    spec:
      topologySpreadConstraints:
        # Hard: Spread across regions
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/region
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: distributed-app
        # Hard: Spread across zones within region
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: distributed-app
        # Soft: Spread across nodes within zone
        - maxSkew: 2
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels:
              app: distributed-app
      containers:
        - name: app
          image: distributed-app:v1

Match Label Expressions

# advanced-label-selector.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: microservice
spec:
  template:
    spec:
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: microservice
            matchExpressions:
              - key: version
                operator: In
                values: ["v1", "v2"]
              - key: environment
                operator: NotIn
                values: ["test"]

minDomains (Kubernetes 1.25+)

# min-domains.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ha-service
spec:
  replicas: 6
  selector:
    matchLabels:
      app: ha-service
  template:
    metadata:
      labels:
        app: ha-service
    spec:
      topologySpreadConstraints:
        - maxSkew: 2
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          minDomains: 3  # Require at least 3 zones
          labelSelector:
            matchLabels:
              app: ha-service
      containers:
        - name: app
          image: ha-service:v1

nodeAffinityPolicy and nodeTaintsPolicy

# affinity-aware-spread.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: gpu-workload
spec:
  template:
    spec:
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: gpu-workload
          # Honor nodeAffinity when calculating spread
          nodeAffinityPolicy: Honor  # or Ignore
          # Honor node taints when calculating spread
          nodeTaintsPolicy: Honor    # or Ignore
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: gpu
                    operator: Exists
      containers:
        - name: app
          image: gpu-workload:v1

StatefulSet with Spread

# statefulset-spread.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: cassandra
spec:
  serviceName: cassandra
  replicas: 6
  selector:
    matchLabels:
      app: cassandra
  template:
    metadata:
      labels:
        app: cassandra
    spec:
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: cassandra
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: cassandra
      containers:
        - name: cassandra
          image: cassandra:4.1

Cluster-Level Defaults

# Set default constraints for all pods (scheduler config)
# /etc/kubernetes/scheduler-config.yaml
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
  - schedulerName: default-scheduler
    pluginConfig:
      - name: PodTopologySpread
        args:
          defaultConstraints:
            - maxSkew: 1
              topologyKey: kubernetes.io/hostname
              whenUnsatisfiable: ScheduleAnyway
          defaultingType: List

Verify Pod Distribution

# Check pod distribution across nodes
kubectl get pods -l app=web-app -o wide

# Check distribution by zone
kubectl get pods -l app=web-app -o json | \
  jq -r '.items[] | "\(.metadata.name)\t\(.spec.nodeName)"' | \
  while read pod node; do
    zone=$(kubectl get node $node -o jsonpath='{.metadata.labels.topology\.kubernetes\.io/zone}')
    echo "$pod $node $zone"
  done | sort -k3

Summary

Topology spread constraints ensure pods distribute across failure domains for high availability. Use maxSkew: 1 for even distribution, combine zone and node constraints for comprehensive coverage, and choose DoNotSchedule for critical workloads.


πŸ“˜ Go Further with Kubernetes Recipes

Love this recipe? There’s so much more! This is just one of 100+ hands-on recipes in our comprehensive Kubernetes Recipes book.

Inside the book, you’ll master:

  • βœ… Production-ready deployment strategies
  • βœ… Advanced networking and security patterns
  • βœ… Observability, monitoring, and troubleshooting
  • βœ… Real-world best practices from industry experts

β€œThe practical, recipe-based approach made complex Kubernetes concepts finally click for me.”

πŸ‘‰ Get Your Copy Now β€” Start building production-grade Kubernetes skills today!

#topology #scheduling #availability #zones #spread
Luca Berton
Written by Luca Berton

Principal Solutions Architect specializing in Kubernetes, AI/GPU infrastructure, and cloud-native platforms. Author of Kubernetes Recipes and creator of CopyPasteLearn courses.

Kubernetes Recipes book cover

Want More Kubernetes Recipes?

This recipe is from Kubernetes Recipes, our 750-page practical guide with hundreds of production-ready patterns.

Luca Berton Ansible Pilot Ansible by Example Open Empower K8s Recipes Terraform Pilot CopyPasteLearn ProteinLens