Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

The Kubernetes Scheduler Explained

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.

Kubernetes Scheduler in the Control PlaneAPI ServerPods, Nodes, Bindingskube-schedulerFilter, Score, BindetcdCluster state storeWorker Node Akubelet runs podWorker Node Bkubelet runs podWorker Node Ckubelet runs podScheduler writes nodeName; kubelet on that node starts the workload
The Kubernetes Scheduler Explained: control-plane flow from unscheduled pod to node binding

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.

Filter and Score PipelinePod QueueUnscheduledFilter PhaseHard constraintsScore PhaseRank nodesBindSet nodeNameCommon Filter Plugins (examples)NodeResourcesNodeAffinityTaintTolerationVolumeScore plugins: ImageLocality, InterPodAffinity, NodePreferAvoidPodsNormalized scores combined with plugin weights
kube-scheduler filter and score phases before binding a pod to a worker node

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:

  1. NodeResourcesFit: checks whether allocatable CPU, memory, and huge pages can satisfy pod requests.
  2. NodeAffinity: enforces required node affinity terms in the pod spec.
  3. TaintToleration: rejects nodes whose taints the pod does not tolerate.
  4. PodTopologySpread: enforces max skew across zones or hostnames when configured as hard constraints.
  5. 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.

MechanismPhaseEffect if violatedTypical use
Required node affinityFilterPod stays PendingGPU nodes, ARM pools, region lock
Preferred node affinityScorePod still schedulesPrefer cheaper spot nodes
Node taints without tolerationFilterPod stays PendingDedicated system or batch pools
Pod anti-affinity (required)FilterPod stays PendingHA: one replica per host
Topology spread (maxSkew hard)FilterPod stays PendingEven distribution across zones
Resource requests exceed allocatableFilterPod stays PendingRight-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.

Taints, Tolerations, and Dedicated PoolsGeneral Node PoolNo taintsBatch Node PoolTaint: dedicated=batchGPU Node PoolTaint: accelerator=nvidiaWeb App PodNo tolerationCron Job PodTolerates batchDashed line = blocked by taint; solid line = allowed placement
How taints and tolerations steer pods toward the correct node pools in Kubernetes

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.

Pending Pod Troubleshooting FlowPod status: PendingCheck FailedSchedulingkubectl describe podResources fit?Requests vs allocatableAffinity / taints?Match labelsAdd nodes or lower requestsCluster autoscalerFix tolerationsOr remove taintsRelax spread rulesOr add zonesFix the constraint named in the event; rescheduling is automatic
Troubleshooting flow when the Kubernetes scheduler cannot find a feasible node

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.

Scheduler Choice by Workload Typekube-scheduler (default)Deployments, StatefulSetsLong-running HTTP APIsStandard affinity and spreadLaravel, WordPress, microservicesDefault for most clustersSpecialized schedulersGang scheduling for ML batchesQueue fairness across tenantsGPU quota managementVolcano, Kueue, custom pluginsAdd only when default failsStart with kube-scheduler; specialize after measured scheduling gaps
When to use default kube-scheduler versus specialized schedulers in Kubernetes clusters

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 FailedScheduling events 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

kube-scheduler is the control-plane component that assigns each unscheduled pod to exactly one worker node by reading pod specs, node capacity, and cluster policy, then writing a binding to the API server.

Yes. kube-scheduler runs on control plane nodes alongside the API server, controller manager, and etcd. It does not run containers; it only decides placement.

Every cycle starts when a pod exists in etcd with an empty spec.nodeName. kube-scheduler watches the API server, builds a list of feasible nodes, and picks the highest-scoring candidate. Filtering removes impossible nodes first; scoring ranks survivors. The binding step is atomic: the scheduler sends a Binding object, the API server updates nodeName, and only then does the kubelet pull images and start containers. If binding fails because another scheduler replica won the race, the pod re-queues. Multiple scheduler replicas can run for high availability; leader election or optimistic concurrency prevents duplicate bindings.

Modern kube-scheduler uses a two-phase Scheduling Framework model. Filter plugins apply hard yes-or-no constraints: NodeResourcesFit checks allocatable CPU and memory against pod requests; NodeAffinity enforces required affinity; TaintToleration rejects untolerated taints; PodTopologySpread and VolumeBinding can block placement on topology or storage grounds. If every node fails filtering, the pod stays Pending and Kubernetes emits a FailedScheduling event naming the failing plugin. Score plugins then rank surviving nodes from 0 to 100, with examples including ImageLocality, InterPodAffinity, and NodeResourcesBalancedAllocation. Preferred affinity and anti-affinity influence scores but do not block scheduling unless paired with required rules.

Repeating FailedScheduling events mean zero nodes passed filtering—the scheduler is working correctly, not broken. The event message names the failing plugin: insufficient CPU, untolerated taint, node affinity mismatch, and volume node affinity conflict are common causes. Each maps to a configuration change. When Horizontal Pod Autoscaling adds replicas faster than nodes join the cluster, filter failures spike and you need more capacity or smaller requests. Storage-related Pending states often trace to PersistentVolume topology—a volume bound in one zone cannot schedule a pod in another. Read FailedScheduling events before restarting anything.

Resource requests matter more than limits during scheduling. A pod with no CPU or memory request may schedule anywhere but can still get throttled or OOM-killed later.

nodeSelector is a simple label match and behaves like required node affinity with the In operator. Node affinity adds richer operators, soft preferences via preferredDuringSchedulingIgnoredDuringExecution, and required rules under requiredDuringSchedulingIgnoredDuringExecution that act as hard filters. Required node affinity blocks scheduling when no node matches; preferred rules only adjust scores. On mixed-instance clusters I maintain, affinity is cleaner than cramming complex placement logic into nodeSelector alone, especially when you want cheaper instance types as a soft preference rather than a hard requirement.

Taints repel pods unless those pods carry matching tolerations. Common effects include NoSchedule for dedicated pools and NoExecute to evict existing pods when a node is drained. Only pods with a matching toleration schedule onto tainted nodes—this is how teams isolate GPU or batch workloads from general application pods. On worker nodes with mixed instance types, taints are cleaner than repeating long label lists in every deployment manifest. If a pod lacks the toleration for a node's taint, TaintToleration filtering rejects that node and the pod stays Pending until you add the toleration or remove the taint.

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—for example, keeping stateful replicas on separate nodes for HA. NodeResourcesFit can also block a second pod if combined requests exceed remaining allocatable CPU or memory. Scheduling density is a trade-off: tighter bin-packing saves cost but increases noisy-neighbour risk if requests are undersized.

Topology spread constraints reduce correlated failure by defining topologyKey (often topology.kubernetes.io/zone), maxSkew, and whenUnsatisfiable. Set DoNotSchedule when even distribution is a hard SLO—the scheduler treats violation as a filter failure. 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. Combined with required anti-affinity for stateful replicas on cost-sensitive clusters, spread constraints keep failure domains separate without manually pinning pods to specific nodes.

Storage topology and volume binding mode can eliminate nodes silently during scheduling. VolumeBinding filter ensures PV topology matches the node for WaitForFirstConsumer volumes. A volume bound in one availability zone cannot schedule a pod in another zone—that is a common Pending cause unrelated to CPU or memory. 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. Fixing volume-related Pending states requires storage or zone changes, not scheduler restarts.

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. Preemption is not a substitute for right-sized resource requests or adequate cluster capacity—it is an emergency mechanism for priority tiers. Without clear PriorityClass ownership and documentation, preemption can surprise application teams whose pods vanish during a capacity crunch.

Start with kubectl describe pod and inspect Events, or kubectl get events filtered by the pod name. FailedScheduling messages map directly to config changes: insufficient CPU means right-size requests or add nodes; untolerated taint means add a toleration or adjust node taints; affinity mismatch means fix label selectors; volume conflicts mean align storage topology with target zones. I see misconfigured requests or forgotten tolerations cause more incidents than broken application code. Document scheduling conventions in a runbook—label schemas, taint keys, default requests, and PriorityClass approval—so on-call engineers fix config instead of restarting kube-scheduler.

Kubernetes 1.19 and later supports multiple profiles in one kube-scheduler process. Each profile enables a different plugin set, and pods select a profile via spec.schedulerName. You can weight score plugins differently per profile—for example, boosting NodeResourcesBalancedAllocation in a default profile while enabling custom filter plugins in a GPU profile. Multiple scheduler replicas run for high availability; only one binds a given pod at a time. Most teams never fork kube-scheduler—they use built-in plugins and profiles first. Custom out-of-tree plugins compile as Go modules and suit platform teams with sustained engineering capacity; smaller agencies usually get more value from labels, taints, and honest resource requests.

Third-party schedulers like Volcano or Kueue target batch and queue-aware workloads needing gang scheduling or fair sharing. Use them for Jobs and CronJobs with those requirements. Keep default kube-scheduler for long-running Deployments. Managed Kubernetes on DigitalOcean, Linode, or bare metal with MetalLB changes how fast new nodes appear when filters fail on capacity—the filter and score pipeline is identical. Extend kube-scheduler with profiles and plugins only after declarative rules in pod specs—affinity, taints, topology spread, and resource requests—are exhausted. Pair scheduling policy with cluster autoscaling and HPA so capacity-related filter failures resolve automatically where possible.

Share this article

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.

Quick Contact Options
Choose how you want to connect me: