
September 02, 2026
10 min read
Table of Contents
By Kokil Thapa | Last reviewed: September 2026
Getting pods to land on the right hardware is one of the most common scheduling challenges in production Kubernetes clusters. Node Affinity and Pod Affinity explained properly means understanding that affinity rules are not just about preference—they are the primary mechanism for enforcing topology constraints, licensing requirements, and high-availability guarantees. Whether you are pinning database replicas to specific storage nodes or spreading web servers across failure domains, getting these rules wrong leads to pending pods, uneven load distribution, and 3 AM pages. This guide covers the exact configuration patterns I use when deploying Laravel applications to Kubernetes, moving beyond documentation theory to battle-tested YAML.
What Is Node Affinity and Pod Affinity Explained in Kubernetes Scheduling?
At its core, Kubernetes scheduling is a constraint-satisfaction problem. The scheduler must find a node that satisfies resource requests, taints/tolerations, and affinity rules simultaneously. Node Affinity replaces the deprecated nodeSelector field with a more expressive syntax that supports operators like In, NotIn, Exists, and Gt. It operates at the node level, evaluating labels attached to Node objects themselves.
Pod Affinity and Pod Anti-Affinity operate at the pod level. Instead of looking at node labels, the scheduler examines the labels of pods already running on candidate nodes. This enables co-location (keeping related services together for low latency) or anti-colocation (spreading replicas across failure domains for resilience). The critical distinction is scope: Node Affinity asks "does this node match?" while Pod Affinity asks "do the pods on this node match?"
In practice, most production clusters need both. On a legal-tech portal I built, worker pods processing document OCR required GPU-equipped nodes (Node Affinity), while the API replicas needed to spread across availability zones to survive zone failures (Pod Anti-Affinity). Treating these as interchangeable is a common mistake that leads to either unschedulable pods or fragile architectures.
How Do You Configure Node Affinity for Hardware-Specific Workloads?
Node Affinity uses two fields: requiredDuringSchedulingIgnoredDuringExecution for hard constraints and preferredDuringSchedulingIgnoredDuringExecution for soft preferences. Hard constraints cause pods to remain Pending if no matching node exists; soft constraints influence scoring but never block scheduling.
Hard Node Affinity for GPU or Specialized Hardware
When workloads absolutely require specific hardware, use hard affinity. This is non-negotiable for ML inference, video transcoding, or licensed software tied to physical dongles. The following manifest pins a pod to nodes labeled gpu=nvidia-a100:
<!-- language: yaml -->
apiVersion: v1
kind: Pod
metadata:
name: ocr-worker
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: gpu
operator: In
values:
- nvidia-a100
- key: topology.kubernetes.io/zone
operator: In
values:
- us-east-1a
- us-east-1b
containers:
- name: ocr
image: ocr-processor:2026.08
resources:
limits:
nvidia.com/gpu: 1 Note the structure: multiple matchExpressions within a single nodeSelectorTerm are ANDed. Multiple nodeSelectorTerms are ORed. This boolean logic trips up many engineers. If you need "GPU type A OR GPU type B", create two separate terms. If you need "GPU AND specific zone", put both expressions in one term.
Soft Node Affinity for Cost Optimization
For batch jobs or non-critical workloads, prefer spot/preemptible instances without blocking scheduling when they're unavailable. Soft affinity uses weights (1–100) to express preference strength:
<!-- language: yaml -->
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80
preference:
matchExpressions:
- key: node.kubernetes.io/lifecycle
operator: In
values:
- spot
- weight: 20
preference:
matchExpressions:
- key: node-size
operator: In
values:
- large The scheduler sums weights for all matching preferences. A node matching both terms scores higher than one matching only the first. In my experience managing cloud hosting costs, soft affinity for spot instances reduces compute spend by 40–60% for background job processors without sacrificing reliability.
When Should You Use Pod Affinity Versus Pod Anti-Affinity?
Pod Affinity co-locates pods; Pod Anti-Affinity separates them. Choosing correctly depends entirely on your failure model and performance requirements. Getting this backwards is worse than having no affinity rules at all.
| Scenario | Mechanism | Topology Key | Risk if Misconfigured |
|---|---|---|---|
| Spread replicas across AZs | Anti-Affinity (required) | topology.kubernetes.io/zone | All replicas in one zone → total outage on zone failure |
| Co-locate app + cache | Affinity (preferred) | kubernetes.io/hostname | Cross-node cache calls add latency; hard rule causes pending pods |
| Separate competing tenants | Anti-Affinity (required) | kubernetes.io/hostname | Noisy neighbor degrades SLA for adjacent tenant |
| Database primary + replica | Anti-Affinity (required) | topology.kubernetes.io/zone | Primary and replica fail together on storage outage |
| Batch workers near data | Affinity (preferred) | topology.kubernetes.io/zone | Cross-zone egress charges; hard rule blocks scaling during zone capacity issues |
A pattern I've seen repeatedly in multi-tenant SaaS applications is using hard Pod Anti-Affinity on kubernetes.io/hostname for tenant isolation. This works until you scale beyond your node count, then new tenant pods pend indefinitely. Always pair hard anti-affinity with a monitoring alert on Pending pod duration, or use soft anti-affinity with high weight (90+) to strongly prefer separation without guaranteeing it.
The Topology Key Trap
Pod Affinity requires a topologyKey. This defines the domain over which the rule applies. Using kubernetes.io/hostname means "same node" or "different node". Using topology.kubernetes.io/zone means "same zone" or "different zone". A common mistake is using hostname-level anti-affinity when you actually need zone-level spreading. Hostname anti-affinity with three replicas on a five-node cluster works fine until two nodes go down for maintenance, then your third replica can't schedule anywhere.
Always match your topology key to your actual failure domain. For cloud deployments, zone-level anti-affinity is usually correct for HA. Hostname-level is correct for noisy-neighbor prevention or cache locality. Never assume hostname equals failure domain—in bare-metal clusters, multiple hosts may share a switch or power circuit.
Why Are My Pods Stuck in Pending After Adding Affinity Rules?
Pending pods after adding affinity rules almost always stem from one of three issues: impossible constraints, label drift, or insufficient topology domains. Debugging requires checking each systematically.
- Describe the pending pod:
kubectl describe pod <name>shows scheduler events. Look for "0/N nodes are available" messages that specify which affinity rule failed. - Verify node labels exist:
kubectl get nodes --show-labelsconfirms your target labels are present. Typos in label keys are invisible in YAML validation but fatal at scheduling time. - Check label values match exactly: Label values are case-sensitive strings.
us-east-1a≠US-EAST-1A. - Count eligible nodes vs required replicas: Hard anti-affinity on hostname with N replicas requires ≥N nodes. If you have 3 replicas and 3 nodes, any node maintenance blocks scheduling.
- Inspect conflicting affinities: Node Affinity requiring zone A combined with Pod Anti-Affinity requiring separation from pods that only exist in zone A creates an impossible state.
On a production eCommerce system, I once spent hours debugging pending checkout-service pods. The Node Affinity targeted tier=frontend nodes, but a recent cluster upgrade had relabeled those nodes to tier=web. The pods weren't broken—the infrastructure had drifted. Now I always include label verification in CI/CD pipeline pre-deploy checks to catch drift before rollout.
Recovery Patterns for Over-Constrained Clusters
When hard affinity causes scheduling deadlocks, you have options beyond deleting rules. Consider downgrading hard rules to soft rules temporarily during maintenance windows. Use preferredDuringSchedulingIgnoredDuringExecution with weight 100 to maintain strong preference without blocking. Alternatively, implement a PriorityClass that allows critical system pods to preempt lower-priority workloads violating soft affinity. This preserves your intent while acknowledging operational reality.
How Does Affinity Interact with Taints, Tolerations, and Topology Spread Constraints?
Affinity does not operate in isolation. Understanding its interaction with other scheduling primitives prevents subtle bugs where rules appear correct individually but conflict in combination.
Taints and Tolerations gate eligibility before affinity evaluation. A node tainted NoSchedule=gpu:true rejects all pods lacking the matching toleration, regardless of Node Affinity. Affinity then selects among the remaining eligible nodes. Always ensure tolerations exist before adding affinity targeting tainted nodes—otherwise pods pend with misleading "no matching affinity" errors when the real issue is missing toleration.
Topology Spread Constraints (TSC) complement Pod Anti-Affinity. Anti-affinity is binary: either pods are separated or they aren't. TSC provides proportional balancing with maxSkew. For spreading 10 replicas across 3 zones, anti-affinity fails (can't separate 10 items into 3 buckets uniquely). TSC with maxSkew: 1 ensures zones differ by at most one replica. In Kubernetes 1.27+, TSC supports minDomains to prevent degenerate distributions when node counts are low.
Resource requests filter before affinity scoring. Even if a node matches all affinity rules, insufficient CPU/memory excludes it. On resource-constrained clusters common in Nepal-based startups optimizing cloud spend, I've seen affinity rules appear broken when the real issue was resource fragmentation. Always check kubectl top nodes alongside affinity debugging.
Implementing Node Affinity and Pod Affinity Explained for Production Reliability
Mastering Node Affinity and Pod Affinity explained through documentation is necessary but insufficient. Production reliability demands treating affinity rules as code: version-controlled, tested, and monitored. Start with soft rules during initial deployment to validate label schemas, then graduate to hard rules only after confirming sufficient topology coverage. Document the business rationale behind every affinity rule in comments or external docs—"spread across zones for HA" is useful context six months later when someone considers relaxing the constraint.
Monitor scheduling latency and pending pod duration as leading indicators of affinity misconfiguration. Set alerts on pods pending >5 minutes due to affinity constraints. Integrate label schema validation into your CI pipeline to catch drift before deployment. When scaling clusters, remember that hard anti-affinity creates implicit minimum node counts—document these dependencies explicitly.
Affinity rules encode architectural decisions about failure domains, performance characteristics, and cost trade-offs. Treat them with the same rigor as application code. For teams deploying PHP/Laravel workloads to Kubernetes, I recommend starting with the patterns in my resource limits and requests guide before layering affinity, since resource constraints interact directly with scheduling eligibility. If you need help designing scheduling policies for your specific workload topology, reach out to discuss your architecture.









