Understanding Kubernetes Scheduling: How the Control Plane Decides Where Your Workload Lives
Pod placement looks like a black box until you see it for what it actually is: a two-stage pipeline of elimination and ranking, running the same way for every pod, every time.
Ask most engineers how Kubernetes decides which node a pod lands on, and you’ll get some version of “it just figures it out.” That answer isn’t wrong, but it hides a genuinely deliberate process. The kube-scheduler isn’t guessing, and it isn’t spreading pods around at random to balance things out. It’s running every pending pod through a strict two-phase pipeline: first eliminate every node that can’t work, then rank whatever’s left.
The Two Phases: Filtering, Then Scoring
Every scheduling decision in Kubernetes happens in two clearly separated stages. The first stage, filtering, asks a binary question of every node in the cluster: could this pod run here at all? Nodes that fail even one filter are dropped immediately, no partial credit, no second chances. The second stage, scoring, only runs on whatever nodes survived filtering, and it asks a completely different question: given several nodes that could all technically run this pod, which one should?
This separation matters because it means “does it fit” and “is it the best fit” are never conflated. A node can score beautifully on paper and still get excluded outright if it fails a single filter, such as not having enough free memory. Only nodes that pass every filter are eligible to be ranked at all.
Filtering: Legacy Predicates, Modern Filter Plugins
Older Kubernetes documentation refers to this stage using the term predicates, and you’ll still see that language in some tooling and blog posts. The modern scheduler implements the same idea as Filter plugins within its plugin framework, but the concept hasn’t changed: a filter plugin either says a node is viable or it says the node is out, with no middle ground. Kubernetes runs these checks in parallel across all filter plugins and all nodes, since it needs this to stay fast even in clusters with thousands of nodes.
Typical filter checks include whether the node has enough allocatable CPU and memory left, whether required volumes can actually be attached from that node, and whether the pod’s node selector or required affinity rules are satisfied. Fail any one of these, and the node never even reaches the scoring stage.
Scoring: Legacy Priorities, Modern Score Plugins
Once filtering narrows the candidate list, the scheduler runs each surviving node through every enabled scoring plugin. Each plugin returns a numeric score for that node, the scores are normalized, and a weighted total is computed. Legacy documentation calls these priority functions; the modern scheduler calls them Score plugins, but again, it’s the same mechanism under a new name.
The pod is bound to whichever node ends up with the highest combined score. If there’s a tie, one of the top-scoring nodes is chosen at random, which is often the moment that gives scheduling its reputation for feeling arbitrary. In reality, the randomness only ever applies to breaking ties among nodes that were already judged equally good.

What Actually Happens Inside the Pipeline
The full scheduling framework has grown well beyond just filter and score. Modern kube-scheduler versions expose extension points across the entire lifecycle of a scheduling decision, from queue sorting before a pod is even considered, through pre-filter and post-filter steps, into scoring, and finally through reserve, permit, and bind. Plugins can attach to more than one extension point if their logic needs to span multiple stages.
This plugin architecture is also what lets alternative schedulers, batch queuing systems, and gang-scheduling tools integrate without forking the scheduler’s source code. They simply register against the same extension points the built-in plugins use, and the core scheduling loop calls them at the appropriate moment, the same way it calls everything else.
| Stage | Question It Answers | Example Plugin |
|---|---|---|
| PreFilter | Is there any reason to abandon scheduling this pod before checking nodes? | Pod topology spread constraint validation |
| Filter | Can this specific node run the pod at all? | NodeResourcesFit, taint toleration checks |
| Score | Among viable nodes, how good a fit is this one? | NodeResourcesFit (scoring mode), InterPodAffinity |
| Reserve / Permit | Should this placement be tentatively held before binding? | Used heavily by batch and gang-scheduling systems |
| Bind | Commit the decision and hand off to the kubelet | DefaultBinder |
To put a number on that plugin architecture: a modern kube-scheduler installation typically ships with around fourteen in-tree Filter plugins and nine in-tree Score plugins, spread across thirteen total extension points in the framework. The chart below shows how that plugin count concentrates at the filtering and scoring stages compared to the rest of the pipeline.

Affinity, Anti-Affinity, and Taints: Steering the Outcome
Filtering and scoring run automatically for every pod, but Kubernetes gives you several ways to influence the outcome directly, rather than leaving it purely to default heuristics.
Node Affinity and Anti-Affinity
Node affinity rules let a pod express a preference or a hard requirement about which nodes it can land on, based on node labels. A required rule behaves like an additional filter, nodes that don’t match are eliminated outright. A preferred rule behaves like an additional scoring input, nudging the ranking without disqualifying anything.
Pod Affinity and Anti-Affinity
Pod affinity and anti-affinity work the same way, but the comparison is against other running pods rather than node labels. This is how you tell the scheduler to keep two services on the same node for latency reasons, or to actively spread replicas of the same deployment across different nodes or failure domains for resilience.
Taints and Tolerations
Taints work in the opposite direction from affinity. A taint is applied to a node and repels pods by default, unless the pod explicitly carries a matching toleration. This is how Kubernetes reserves nodes for specific workloads, such as GPU nodes that should only run workloads that actually declare a toleration for that taint, keeping every other pod out without needing to touch each pod’s own configuration.
Quick Checklist: Reasoning About a Placement Decision
- Check whether a pod is Pending due to a failed filter, not a scoring outcome, since these require different fixes.
- Remember required affinity and taints act like filters; they can eliminate a node entirely, not just rank it lower.
- Preferred affinity only ever influences scoring, so it can be outweighed by other scoring plugins.
- A tie in final scores is resolved randomly among the tied nodes only, not across the whole cluster.
- If placement behavior seems inconsistent, check which scheduler profile and which plugins are actually enabled, since these are configurable per cluster.
Why Placement Isn’t Random, Even When It Looks Like It
The scheduler’s default scoring plugin for resource usage typically favors spreading workloads across nodes rather than packing them tightly, prioritizing resilience against node failure over squeezing out maximum utilization. Tighter bin-packing behavior exists too, but it’s an opt-in scoring mode rather than the default, which is a deliberate trade-off, not an oversight.
It’s also worth remembering that the scheduler works from what a pod requests, not what it actually consumes. A pod that requests two CPUs but only uses a fraction of that still counts as a two-CPU pod for every filtering and scoring decision, since the scheduler has no visibility into runtime usage at decision time. This is frequently the real explanation behind a node that looks underused yet still refuses new pods: the requests, not the actual load, are what the scheduler is reasoning about.
What We Learned
Kubernetes scheduling isn’t a mystery once you separate what it’s actually doing into its two real phases: eliminate anything that can’t work, then rank whatever’s left. Affinity rules and required node selectors act as extensions of the filtering stage, taints repel by default unless explicitly tolerated, and preferred affinity only ever nudges the scoring stage. The apparent randomness people notice is almost always tie-breaking among nodes that were already judged equally suitable, not evidence that placement is arbitrary. Once you see the pipeline for what it is, pod placement stops looking like a black box and starts looking like exactly what it is: a consistent, inspectable decision process running the same way every single time.



