
September 10, 2026
12 min read
By Kokil Thapa | Last reviewed: September 2026
When a pod stays in Pending, the problem is often scheduling—not your container image. The Kubernetes Scheduler Explained in plain terms is the control-plane component that picks exactly one worker node for each unscheduled pod. It reads pod specs, node capacity, and cluster policy, then writes a binding to the API server. If you run Laravel on Kubernetes or manage a mixed cluster for client workloads, scheduler behaviour directly affects uptime, cost, and noisy-neighbour risk. This guide walks through the real pipeline, the knobs you can turn, and the failure modes I see on production clusters.
The scheduler sits beside the API server and etcd in the Kubernetes control plane architecture. It does not run containers. It only decides where they should run. That separation keeps scheduling logic replaceable and testable. On clusters I help maintain, misconfigured requests or forgotten tolerations cause more incidents than broken application code.
How does the Kubernetes scheduler decide which node runs a pod?
Every scheduling cycle starts when a pod exists in etcd with an empty spec.nodeName. The default in-tree scheduler is kube-scheduler. It watches the API server for pods in the scheduling queue. For each pod, it builds a list of feasible nodes and picks the highest-scoring candidate.
The binding step is atomic at the API level. The scheduler sends a Binding object. The API server updates the pod's nodeName. Only then does the kubelet on that node start pulling images and creating containers. If binding fails because another scheduler instance won the race, the pod re-queues and the cycle repeats.
Multiple scheduler replicas can run for high availability. Only one binds a given pod at a time. Leader election or optimistic concurrency prevents duplicate bindings. The etcd data store holds the authoritative pod and node objects the scheduler reads.
Inputs the scheduler reads every cycle
The scheduler consults several object types before it commits. Each one can eliminate a node silently if you forget to configure it.
- Pod spec: resource requests and limits, affinity rules, tolerations, priority class, runtime class, and topology spread constraints.
- Node status: allocatable CPU, memory, and ephemeral storage after system reservations.
- Node labels and taints: used for targeting GPU nodes, spot instances, or dedicated pools.
- Existing pods on nodes: for affinity, anti-affinity, and port conflict checks.
- Storage: PersistentVolume topology and volume binding mode affect whether a pod can land on a node at all.
Resource requests matter more than limits during scheduling. A pod with no CPU or memory request may schedule anywhere. It can still get throttled or OOM-killed later. See Kubernetes resource limits and requests for the full picture on capacity planning.
What are the filtering and scoring phases in kube-scheduler?
Modern kube-scheduler uses a two-phase model inside the Scheduling Framework. First, filtering removes nodes that violate hard constraints. Second, scoring ranks the survivors. The highest score wins unless you enable multiple profiles or custom plugins that change tie-breaking.
Filtering: hard constraints that drop nodes
Filter plugins answer yes-or-no questions. If any filter fails for a node, that node is out. Typical filters include:
- NodeResourcesFit: checks whether allocatable CPU, memory, and huge pages can satisfy pod requests.
- NodeAffinity: enforces required node affinity terms in the pod spec.
- TaintToleration: rejects nodes whose taints the pod does not tolerate.
- PodTopologySpread: enforces max skew across zones or hostnames when configured as hard constraints.
- VolumeBinding: ensures PV topology matches the node for WaitForFirstConsumer volumes.
If every node fails filtering, the pod stays Pending. Kubernetes emits a FailedScheduling event with the reason. That event is your first debug signal. The companion article on debugging CrashLoopBackOff covers pod lifecycle issues after scheduling; Pending state is earlier in the chain.
Scoring: soft preferences and tie-breakers
Score plugins assign integers from 0 to 100 per node. The scheduler normalizes and weights them. Examples include ImageLocality (prefer nodes that already cached the image), InterPodAffinity (co-locate or spread pods), and NodeResourcesBalancedAllocation (spread load evenly).
Preferred node affinity and preferred pod anti-affinity also influence scores. They never block scheduling unless paired with required rules. On cost-sensitive clusters, I often combine preferred zone spread with required anti-affinity for stateful replicas.
| Mechanism | Phase | Effect if violated | Typical use |
|---|---|---|---|
| Required node affinity | Filter | Pod stays Pending | GPU nodes, ARM pools, region lock |
| Preferred node affinity | Score | Pod still schedules | Prefer cheaper spot nodes |
| Node taints without toleration | Filter | Pod stays Pending | Dedicated system or batch pools |
| Pod anti-affinity (required) | Filter | Pod stays Pending | HA: one replica per host |
| Topology spread (maxSkew hard) | Filter | Pod stays Pending | Even distribution across zones |
| Resource requests exceed allocatable | Filter | Pod stays Pending | Right-size before HPA scales out |
When Horizontal Pod Autoscaling adds replicas faster than nodes join the cluster, filter failures spike. The scheduler is doing its job. You need more capacity or smaller requests.
How do you configure node affinity, taints, and topology spread?
Declarative scheduling rules live in the pod spec or in a mutating admission webhook. You rarely touch scheduler flags for day-to-day placement. Instead you express intent through affinity, tolerations, and spread constraints.
Node affinity example
Required rules go under requiredDuringSchedulingIgnoredDuringExecution. The scheduler treats them as filters. Preferred rules only adjust scores.
apiVersion: v1
kind: Pod
metadata:
name: api-worker
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: workload
operator: In
values: ["compute"]
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80
preference:
matchExpressions:
- key: node.kubernetes.io/instance-type
operator: In
values: ["standard-4"]
tolerations:
- key: "dedicated"
operator: "Equal"
value: "batch"
effect: "NoSchedule"
containers:
- name: app
image: myregistry/api:2026.09
resources:
requests:
cpu: "500m"
memory: "512Mi"
Validate YAML structure with a JSON formatter when converting from Helm output or CI templates. Small indentation errors surface late if you skip schema checks.
Taints on nodes and tolerations on pods
Taints repel pods unless those pods carry matching tolerations. Common patterns include NoSchedule for dedicated pools and NoExecute to evict existing pods when a node is drained.
kubectl taint nodes node-gpu-01 accelerator=nvidia:NoSchedule
Only pods with the matching toleration schedule there. This is how teams isolate GPU scheduling workloads from general application pods. On a worker node architecture with mixed instance types, taints are cleaner than long label lists in every deployment manifest.
Topology spread for zone and host balance
Topology spread constraints reduce correlated failure. You define topologyKey (often topology.kubernetes.io/zone), maxSkew, and whenUnsatisfiable.
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: payment-api
Set DoNotSchedule when even distribution is a hard SLO. Use ScheduleAnyway when you prefer balance but accept imbalance during scale events. For booking platforms and API backends on Kubernetes, I default to zone spread with two replicas minimum per critical service.
What happens when the Kubernetes scheduler cannot place a pod?
A Pending pod with repeating FailedScheduling events means zero nodes passed filtering. The event message names the failing plugin. Read it before you restart anything.
Diagnose with kubectl
kubectl describe pod payment-api-7f8c9d-xyz | grep -A5 Events
kubectl get events --field-selector involvedObject.name=payment-api-7f8c9d-xyz
Common messages include insufficient CPU, untolerated taint, node affinity mismatch, and volume node affinity conflict. Each maps to a config change—not a scheduler bug.
Preemption enters the picture when high-priority pods need space. The scheduler may evict lower-priority pods on a node, then bind the urgent pod. This is disruptive. Define PriorityClass objects carefully and document which teams own each class.
Storage-related Pending states often trace to PersistentVolume topology. A volume bound in zone ap-south-1a cannot schedule a pod in ap-south-1b. For Laravel apps with local file storage on Kubernetes, prefer shared storage classes or object storage rather than node-local disks unless you accept the topology lock.
How do you customize or extend the Kubernetes scheduler?
Most teams never fork kube-scheduler. They use built-in plugins, scheduler profiles, or a second scheduler for special workloads. Extension points in the Scheduling Framework include pre-filter, filter, post-filter, pre-score, score, reserve, permit, and bind.
Scheduler profiles and multiple schedulers
Kubernetes 1.19+ supports multiple profiles in one kube-scheduler process. Each profile enables a different plugin set. Pods select a profile via spec.schedulerName.
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: default-scheduler
plugins:
score:
enabled:
- name: NodeResourcesBalancedAllocation
weight: 2
- schedulerName: gpu-scheduler
plugins:
filter:
enabled:
- name: NodeResourcesFit
Third-party schedulers like Volcano or Kueue target batch and queue-aware workloads. Use them for Jobs and CronJobs that need gang scheduling or fair sharing. Keep default kube-scheduler for long-running Deployments.
Custom scheduler plugins compile as out-of-tree modules in Go. You register them in the scheduler configuration and rebuild the binary. That path suits platform teams with sustained engineering capacity. Smaller agencies running client clusters on Linux system administration contracts usually get more value from labels, taints, and requests than from custom code.
Connecting scheduler behaviour to application deploys
On a production Laravel deployment, scheduler settings interact with PHP-FPM memory, queue workers, and Horizon processes. Each should declare honest memory requests. Undersized requests pack too many pods per node and cause latency spikes under load. The Kubernetes for Laravel getting started guide pairs well with right-sized resource blocks.
For enterprise platforms with strict tenancy, combine RBAC policies with namespace-level ResourceQuotas. Quotas cap how much schedulable capacity a team can consume. They do not replace node-level taints for hardware isolation.
Tune performance after placement stabilizes. Kubernetes performance tuning covers kubelet and CNI settings once pods actually land on nodes. Scheduling fixes come first; micro-optimizations second.
If you operate clusters for clients without a dedicated platform team, document scheduling conventions in a runbook. Include label schemas, taint keys, default requests, and who approves PriorityClass changes. On sister sites I deploy with GitLab CI and Deployer, the Kubernetes layer is separate—but the same discipline applies: predictable placement beats heroic debugging after an outage.
Managed Kubernetes on DigitalOcean, Linode, or bare metal with MetalLB changes node provisioning speed, not scheduler logic. The filter and score pipeline is identical. What changes is how fast new nodes appear when filters fail on capacity.
Key Takeaways
- The Kubernetes scheduler filters impossible nodes first, then scores survivors—read
FailedSchedulingevents to see which filter blocked placement. - Resource requests, not limits, drive scheduling—set honest CPU and memory requests on every production container.
- Use taints and tolerations for dedicated pools; use required affinity and topology spread only when violation should block scheduling entirely.
- Pending pods with volume topology conflicts need storage or zone changes, not scheduler restarts.
- Extend kube-scheduler with profiles and plugins only after declarative rules in pod specs are exhausted.
- Pair scheduling policy with cluster autoscaling and HPA so filter failures from capacity gaps resolve automatically where possible.
People Also Ask
Is the Kubernetes scheduler part of the control plane?
Yes. kube-scheduler runs on control plane nodes alongside the API server, controller manager, and etcd. It is stateless regarding workload execution. It only assigns pods to nodes. Worker kubelets perform the actual container start.
Can two pods schedule to the same node?
Yes, unless anti-affinity, topology spread, or resource filters prevent it. Most clusters intentionally bin-pack multiple pods per node to improve utilisation. Required pod anti-affinity is the usual tool when replicas must not share a host.
What is the difference between nodeSelector and node affinity?
nodeSelector is a simple label match and behaves like required node affinity with the In operator. Node affinity adds operators, soft preferences, and richer boolean logic. New manifests should prefer affinity; nodeSelector remains supported for legacy specs.
Does the scheduler respect PodDisruptionBudgets?
Not during initial scheduling. PDBs constrain voluntary evictions during drains and disruptions. They do not block first-time pod placement. Preemption for priority classes is a separate mechanism with its own rules.
Put scheduling policy to work on your cluster
The Kubernetes Scheduler Explained boils down to one loop: watch unscheduled pods, filter nodes, score what remains, bind the best fit. Most production pain comes from missing requests, forgotten tolerations, and hard spread rules on undersized clusters—not from scheduler internals. Fix the declarative spec first; customize kube-scheduler only when you have measured gaps.
If you are moving a Laravel booking platform, legal-tech portal, or eCommerce API onto Kubernetes and want placement, storage, and autoscaling designed together, review the Adventure Third Pole Trek deployment case study or explore enterprise application development services. For ongoing cluster care after go-live, support and maintenance keeps scheduler-related drift visible before pods pile up in Pending. Contact us to audit your manifests, node pools, and scheduling events before the next scale event catches you off guard.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

