Scheduler Framework Plugins in Kubernetes
How Kubernetes decides which machine runs each of your Pods, and how a chain of small, swappable plugins turns that decision into a fast, tunable, extensible pipeline. A complete, step-by-step guide from the first idea to production tuning.
Imagine you run a very large hotel with thousands of rooms. Guests arrive all day long, and each one has different needs. One wants a quiet room, another needs two beds, a third must be on the ground floor because of a wheelchair, and a famous guest should get the best suite even if it means moving someone else. The person at the front desk has to match every guest to a room quickly, fairly, and without making mistakes. That person is a scheduler.
Kubernetes has exactly such a person. Every time you create a Pod, which is the smallest unit of work in Kubernetes, someone has to decide which machine in the cluster will run it. That decision maker is the kube-scheduler. In the early days it was a single block of hard-coded rules. Today it is a pipeline of small parts called plugins, arranged inside a design called the scheduling framework. Each plugin does one small job, such as “reject machines that are too full” or “prefer machines that already have the image downloaded.”
In this guide you will learn what the scheduling framework is, why it was created, what every extension point does, what the built-in plugins do, how to tune them through configuration, how to write or install your own, and how to avoid the mistakes that cause Pods to stay stuck in the Pending state. Every new word is explained the first time it appears, so you can read from top to bottom with no earlier knowledge of scheduler internals.
1The Big Idea: What Is the Scheduler Framework?
A scheduler is a matchmaker. The framework is the set of stages the matchmaker goes through, and plugins are the specialists at each stage.
A Pod is a small package that holds one or more containers. A node is a machine, physical or virtual, that can run Pods. A new Pod starts life with no node assigned. The scheduler is the control plane component that looks at every unassigned Pod, picks the best node for it, and writes that choice back into the cluster. After that, the node’s own agent, called the kubelet, takes over and actually starts the containers.
The scheduling framework is the way the scheduler organizes its thinking. Instead of one giant function, the work is split into named stages. Each stage is called an extension point, which is simply a spot where plugins can attach. A plugin is a small piece of logic that registers itself at one or more extension points. When the scheduler reaches a stage, it calls every plugin registered there, collects the answers, and moves on.
Think of airport security. Every traveler passes through a fixed series of stages: check the ticket, scan the bag, walk through the metal detector, and board the plane. The stages never change. But the officers working at each stage can be replaced or added to, and each officer checks one particular thing. The scheduling framework is the airport layout, and the plugins are the officers.
Two big questions the scheduler answers
Every scheduling decision answers two questions in order. The first is “Which nodes are even allowed to run this Pod?” That is called filtering. The second is “Among the allowed nodes, which one is best?” That is called scoring. Almost every built-in plugin exists to help answer one of these two questions, and understanding this split is the key to understanding everything else in this guide.
Filtering says yes or no. Scoring says how good. A node that fails filtering is never scored, and a node that passes filtering can still lose to a better scoring node.
2Why the Framework Exists: From Hard-Coded Rules to Plugins
Understanding the problem makes the design feel obvious.
In the early versions of Kubernetes, the scheduler used two lists of rules. The first list, called predicates, were yes-or-no tests. The second list, called priorities, were scoring functions. These were written directly into the scheduler’s source code. If your company needed a special rule, such as “never place a database Pod on a machine that also runs a web server owned by another team,” you had two bad choices. You could change the scheduler’s source code and keep your own modified copy forever, or you could attach an outside web service to the scheduler.
The outside web service approach was called a scheduler extender. The scheduler would send a network request carrying the Pod and a list of nodes, and the extender would reply with a shorter list or with scores. Extenders worked, but they had real limits. They were slow because every call crossed the network. They could only join at a few fixed points. They could not see the scheduler’s internal cache. And they could not take part in the later steps, such as reserving space or holding a Pod until a group of Pods was ready.
Imagine a pizza shop where the recipe is carved into stone. To add a new topping you must break the stone. An extender is like phoning a friend across town to ask whether pineapple is allowed, every single time an order arrives. A plugin framework is like giving the kitchen a clearly labeled rack where any chef can place a new ingredient, right next to the oven.
| Question | Old hard-coded rules | Scheduler extender | Scheduling framework plugins |
|---|---|---|---|
| How do you add a rule? | Edit and rebuild the scheduler | Run a separate web service | Register a plugin at an extension point |
| Speed | Fast | Slower, network round trip | Fast, runs in the same process |
| Access to scheduler state | Full but tangled | Only what is sent in the request | Shared cache and per-cycle state |
| Where can it take part? | Two places | A few places | Every stage of the pipeline |
| Turn a rule off | Change the code | Remove the extender | Change the configuration file |
The Kubernetes community introduced the scheduling framework to solve these problems. It rebuilt the scheduler so that even its own built-in behavior is made of plugins. This is an important point: the default scheduler is not special. It is simply a framework running a default list of plugins. When you tune it, you are changing which plugins run and how they are weighted. When you extend it, you are adding one more plugin to the same list the built-in ones use.
The old predicates and priorities were translated into plugins. For example, the old resource-fit predicate became the NodeResourcesFit plugin, and the old taint predicate became part of the TaintToleration plugin. If you read older blog posts using the old words, you can map them to these plugin names.
3Core Vocabulary You Will Meet Everywhere
Eight short words that unlock every scheduler conversation.
Pod
The smallest deployable unit in Kubernetes. One or more containers that always run together on the same node.
Node
A machine in the cluster that has CPU, memory, and other capacity to offer to Pods.
Resource requests
The amount of CPU and memory a Pod says it needs. The scheduler plans using these numbers, not the live usage.
Affinity and anti-affinity
Preferences or requirements about being placed near or far from certain nodes or other Pods.
Taints and tolerations
A taint is a “keep out” sign on a node. A toleration on a Pod says “I am allowed in despite that sign.”
Priority
A number on a Pod that says how important it is. Higher priority Pods are scheduled first and may push out lower ones.
Extension point
A named stage of the scheduling pipeline where plugins can attach, such as Filter or Score.
Profile
A named bundle of plugin settings. One scheduler process can run several profiles, each with its own behavior.
Requests, not real usage
A beginner surprise: the scheduler does not look at how busy a node is right now. It looks at the sum of the resource requests of the Pods already placed there and compares that with what the node can offer. A node may be using only ten percent of its real CPU yet be “full” for scheduling because the Pods on it have asked for all of it. This is deliberate. Requests are stable promises, while live usage jumps up and down every second.
A restaurant takes table reservations. If every table is reserved for eight o’clock, the host turns new bookings away, even if at six o’clock half the tables look empty. The host plans around reservations, not around who is sitting there at this moment.
Status codes
When a plugin finishes, it returns a status, which is a small answer with a code. The important codes are easy to remember. Success means all is well. Unschedulable means “this node does not work for this Pod, but things might change.” UnschedulableAndUnresolvable means “no change can ever fix this, so do not bother trying other fixes such as preemption.” Error means something broke inside the plugin. Wait means “hold the Pod for a while.” Skip means “I have nothing to do for this Pod, so skip my later stages.” These codes control what the scheduler does next.
4Architecture: Where the Scheduler Sits in the Cluster
The scheduler is one program that listens to the API server and speaks only to it.
Kubernetes has a control plane, which is the brain, and many worker nodes, which are the muscle. Inside the control plane, the API server is the single front door and the etcd database is the single source of truth. The kube-scheduler never reads the database directly. It watches the API server, which means it keeps a long-lived connection and receives a message each time a Pod or a node changes.
flowchart TB
U["User or controller creates a Pod"] --> API["kube-apiserver"]
API ETCD[("etcd datastore")]
API -->|"watch Pods Nodes Volumes"| INF["Scheduler informers"]
INF --> CACHE["Scheduler cache and node snapshot"]
INF --> QUEUE["Scheduling queues"]
QUEUE --> FW["Framework with plugins"]
CACHE --> FW
FW -->|"bind Pod to node"| API
API -->|"watch Pods on this node"| KL["kubelet on chosen node"]
KL --> CR["Container runtime"]
Informers keep a local picture
The scheduler holds copies of Pods, nodes, volumes, and other objects in memory. It updates them from watch messages, so it almost never has to ask the API server for data while thinking.
The cache tracks what each node holds
For every node, the cache stores its capacity and the total requests of Pods placed there. At the start of each decision, the scheduler takes a snapshot so that the view stays steady while plugins work.
The queues hold waiting Pods
Every Pod without a node is placed into a queue. The scheduler takes one Pod at a time from the front of the queue.
The framework runs the plugins
The framework walks the Pod through its stages, calling each registered plugin and combining the answers.
The binder writes the result
The final choice is written to the API server as a binding, which sets the Pod’s node name. From that moment the kubelet on that node sees the Pod and starts it.
A hotel front desk has a computer screen that shows which rooms are free, a line of guests waiting, and a rulebook. The receptionist does not walk to every room to check. The screen is updated by housekeeping in the background. The cache is that screen, the queue is that line, and the framework is the rulebook with its specialists.
Managed services such as Amazon EKS and Google GKE run the default scheduler inside their managed control plane. Customers cannot usually log in to that machine, but they can still influence scheduling through Pod settings such as resource requests, affinity, taints, topology spread, and priority. Customers who need custom behavior commonly run a second scheduler as an ordinary workload in the cluster.
5The Journey of a Pod Through the Scheduling Queues
A Pod that cannot be placed does not vanish. It waits in the right line until things change.
The scheduler does not simply keep one list. It keeps three, and a Pod moves between them depending on what happens. Understanding the three lines explains why a Pod sometimes gets placed within milliseconds and why another one seems to sleep for minutes.
flowchart TD
NP["New Pod without a node"] --> PE{"PreEnqueue gates pass?"}
PE -->|"No"| GT["Held back as gated"]
PE -->|"Yes"| AQ["activeQ ordered by QueueSort"]
AQ --> CY["Scheduling cycle"]
CY -->|"Node chosen"| BD["Binding cycle"]
CY -->|"No node fits"| UP["Unschedulable pool"]
UP -->|"Cluster event may help"| BQ["backoffQ"]
UP -->|"Waited too long"| BQ
BQ -->|"Backoff time over"| AQ
BD -->|"Binding failed"| BQ
BD -->|"Bound"| DN["Pod running on node"]
activeQ
Pods that are ready to be tried right now, sorted so the most important Pod is at the front.
backoffQ
Pods that recently failed and are waiting out a short penalty before the next try. The penalty starts near one second and doubles up to about ten seconds by default.
Unschedulable pool
Pods that failed because no node fit. They wait here until an event might help them, such as a new node joining or a Pod being deleted.
PreEnqueue gate
A checkpoint before the activeQ. A Pod that has scheduling gates attached is held outside until someone removes the gates.
Why the unschedulable pool matters
Imagine a Pod that asks for eight GPUs, and the cluster has none free. Trying it again every second would waste the scheduler’s time and block everyone else. So the scheduler parks it in the unschedulable pool. Plugins can tell the scheduler which kinds of events could possibly help that Pod. If a node is added, a Pod is removed, or a label changes, the scheduler checks whether the event matters to the parked Pod and moves it out if so. As a safety net, any Pod that has waited in the pool for about five minutes is moved to be tried again anyway.
At a busy clinic, patients who need a specialist who is not in today are asked to sit in a side waiting area. The receptionist does not call the specialist every minute. She waits until the specialist arrives, or until an hour has passed, and then checks again. The side area is the unschedulable pool.
Scheduling gates
A scheduling gate is a label-like marker in the Pod’s specification that says “do not try to schedule me yet.” An outside controller can attach a gate when a Pod is created and remove it later, once some condition is true, such as “the quota system approved this job.” A built-in plugin called SchedulingGates works at the PreEnqueue extension point and keeps gated Pods out of the active queue entirely, so they cost the scheduler nothing while they wait.
Batch job systems use gates so that thousands of queued jobs do not flood the scheduler. Only the jobs that the queueing system has admitted get their gates removed, and only then does the scheduler see them.
6The Extension Points: The Map of the Whole Pipeline
Twelve named stages, split into two cycles. Learn this map once and every plugin makes sense.
The work of placing one Pod is split into two parts. The scheduling cycle chooses a node. It runs for one Pod at a time, in strict order, because each decision changes the picture for the next Pod. The binding cycle then carries out the decision. It can run for many Pods at once, because it mostly waits on the network. Together they form one scheduling attempt.
flowchart LR
subgraph SC["Scheduling cycle - one Pod at a time"]
PE["PreEnqueue"] --> QS["QueueSort"]
QS --> PF["PreFilter"]
PF --> F["Filter"]
F --> PoF["PostFilter when no node fits"]
F --> PS["PreScore"]
PS --> S["Score"]
S --> N["NormalizeScore"]
N --> R["Reserve"]
R --> P["Permit"]
end
subgraph BC["Binding cycle - can run in parallel"]
PB["PreBind"] --> B["Bind"]
B --> PO["PostBind"]
end
P --> PB
| Extension point | Plain-words job | Typical answer |
|---|---|---|
| PreEnqueue | Decide whether the Pod may enter the active queue at all | Allow or hold back |
| QueueSort | Decide the order of Pods in the queue | Which of two Pods goes first |
| PreFilter | Prepare information once per Pod and reject early if the Pod is hopeless | Success, fail, or a shorter node list |
| Filter | Say yes or no for each node | Fits or does not fit |
| PostFilter | Try to rescue a Pod when no node fits | Nominate a node after making room |
| PreScore | Prepare information once before scoring | Success or skip |
| Score | Give each remaining node a number | 0 to 100 per node |
| NormalizeScore | Rescale a plugin’s numbers into the allowed range | Adjusted scores |
| Reserve | Hold the chosen resources so nobody else takes them | Success or fail, with an undo step |
| Permit | Final approval before binding | Approve, deny, or wait |
| PreBind | Do last preparation work before binding | Success or fail |
| Bind | Write the Pod-to-node link | Success, fail, or skip |
| PostBind | Clean up or send notices after success | Nothing, informational |
Booking a hotel room has the same two phases. In the deciding phase, the receptionist looks at the guest, narrows the rooms, picks the best, and marks it “held.” In the carrying out phase, she prints the key card, updates the system, and tells housekeeping. The deciding phase must be done one guest at a time, so two guests are not given the same room. The carrying out phase can be done for many guests in parallel.
A single plugin can attach to several extension points. For example, the NodeAffinity plugin filters out nodes that break a required rule, and also scores nodes that match a preferred rule. The stages are separate, but the plugin is one piece of code that understands the whole topic of node affinity.
7Filtering Deep Dive: PreFilter, Filter, and PostFilter
Filtering is the stage where impossible nodes are thrown out.
PreFilter: do the heavy thinking once
Suppose a Pod has a complicated rule that says “do not place me on a node that already has a Pod from the same team.” To check one node, you must look at all Pods across the cluster. Doing that scan once per node, for thousands of nodes, would be wasteful. A PreFilter plugin does the heavy scan a single time per Pod and stores the result. Later, the Filter plugin simply reads the stored result for each node.
PreFilter plugins can also act as early guards. If a Pod asks for something that is impossible in the whole cluster, such as a volume claim that does not exist, the PreFilter plugin can fail the Pod right away, saving the effort of testing every node. A PreFilter plugin can even hand back a shorter list of candidate nodes. For example, if a Pod names a specific node, the scheduler only needs to test that node.
Before testing every door in a long corridor with a master key, a security guard first checks the guest’s reservation paper once. If the paper is invalid, there is no point trying any door. If the paper says “third floor only,” the guard skips the other floors.
Filter: the yes-or-no test on every node
The Filter stage asks every Filter plugin about every node. A node is kept only if every plugin says yes. The scheduler runs these checks in parallel, using many workers at once, because the same question is asked about thousands of nodes. For safety, a Filter plugin must not change anything. It only looks and answers.
NodeResourcesFit
Is there enough unreserved CPU, memory, and other resources on the node for this Pod’s requests?
NodeName and NodeUnschedulable
If the Pod names a node, only that node passes. Nodes marked as unschedulable, such as those being drained, are rejected.
NodeAffinity
Does the node’s labels satisfy the Pod’s required node selector and required affinity rules?
TaintToleration
Does the Pod tolerate every hard taint on the node? A node with a taint the Pod cannot tolerate is rejected.
NodePorts
If the Pod needs a specific host port, is that port free on the node?
PodTopologySpread and InterPodAffinity
Would placing the Pod here break a hard spreading rule or a required rule about other Pods?
Volume plugins
VolumeBinding, VolumeRestrictions, VolumeZone, and NodeVolumeLimits check that storage can attach on this node, in the right zone, within attach limits.
DynamicResources
For devices such as GPUs managed through resource claims, does this node offer a matching device?
The shortcut: not every node is examined
In a huge cluster, testing every single node would take too long. The scheduler therefore stops searching once it has found enough nodes that pass the filters. How many are enough is controlled by a setting called percentageOfNodesToScore. When left at its default, the scheduler uses a sliding rule: in small clusters it looks at about half of the nodes, and the share shrinks as the cluster grows, down to a minimum of five percent. It also always tries to find at least one hundred suitable nodes. In a cluster with fewer than a hundred nodes it simply examines all of them.
To avoid always favoring the same nodes at the start of the list, the scheduler remembers where it stopped last time and begins the next search from there, going round in a circle. This keeps the load spread across the cluster.
PostFilter: the rescue stage
If the Filter stage leaves no node at all, the Pod is not simply rejected. The scheduler calls the PostFilter plugins, which are the rescue team. The built-in one, called DefaultPreemption, tries to make room by removing less important Pods. You will meet it fully in Chapter 12. PostFilter only runs when filtering finds nothing, so on a healthy cluster it is rarely used.
When a Pod stays Pending, its event reads like “0/20 nodes are available: 8 Insufficient cpu, 10 node(s) had untolerated taint, 2 node(s) didn’t match Pod’s node affinity.” That message is the list of Filter plugin answers, counted. Read it carefully: it tells you exactly which rule blocked which group of nodes.
8Scoring Deep Dive: PreScore, Score, NormalizeScore, and Weights
When several nodes are acceptable, scoring decides which one wins.
After filtering, usually many nodes remain. Now the scheduler must choose one. It does this by asking every Score plugin to give each remaining node a number from 0 to 100. Higher is better. A node’s final score is the sum of what each plugin said, after each plugin’s number is multiplied by a weight that you can configure. The node with the highest total wins. If two nodes tie, the scheduler picks one of them at random, which helps spread load fairly.
Think of judges at a talent show. Each judge scores every performer from zero to a hundred on a different quality: singing, stage presence, and originality. The show decides that singing counts double. The totals are added up, and the highest total wins. The judges are score plugins, and “counts double” is the weight.
PreScore and NormalizeScore
PreScore works like PreFilter: it does shared preparation once, using the list of nodes that survived filtering, so that scoring each node is quick. NormalizeScore is an optional step that belongs to a Score plugin. Some plugins compute raw numbers in odd ranges, such as the count of matching Pods. Normalization rescales those raw numbers into the 0 to 100 range so that plugins can be fairly added together.
The main scoring plugins
NodeResourcesFit
Scores nodes by how much capacity is used. The default strategy, LeastAllocated, prefers emptier nodes and so spreads Pods out. MostAllocated prefers fuller nodes and so packs Pods together. RequestedToCapacityRatio lets you draw your own curve.
NodeResourcesBalancedAllocation
Prefers nodes where CPU and memory usage would end up balanced, so a node is not left with lots of spare memory and no spare CPU.
ImageLocality
Prefers nodes that already have the Pod’s container images downloaded, which makes startup faster.
NodeAffinity
Gives a bonus to nodes that match the Pod’s preferred, not required, affinity rules.
TaintToleration
Prefers nodes with fewer soft taints that the Pod does not tolerate.
InterPodAffinity
Rewards nodes close to Pods you want to be near, and penalizes nodes close to Pods you want to avoid, when the rules are preferred rather than required.
PodTopologySpread
Rewards nodes in zones or groups that currently hold fewer matching Pods, to keep copies of a service spread out.
VolumeBinding
Where relevant, prefers nodes that make better use of pre-created local volumes.
Hard rule versus soft rule: one plugin, two stages
Several plugins appear in both lists. The reason is that Kubernetes lets you write the same kind of rule in two strengths. A required rule becomes a Filter: break it and the node is out. A preferred rule becomes a Score: break it and the node just loses points. Topology spread shows this clearly. Setting it to “do not schedule” when the spread cannot be kept makes it a hard Filter. Setting it to “schedule anyway” turns it into a soft Score.
| Topic | As a hard rule (Filter) | As a soft rule (Score) |
|---|---|---|
| Node labels | Required node affinity or node selector | Preferred node affinity |
| Taints | NoSchedule and NoExecute taints | PreferNoSchedule taints |
| Other Pods | Required Pod affinity or anti-affinity | Preferred Pod affinity or anti-affinity |
| Spread across zones | Topology spread that refuses to place when skew is too high | Topology spread that places anyway but prefers balance |
The same scoring system can produce opposite behaviors. Spreading Pods across many nodes improves resilience, because one failed machine harms fewer Pods. Packing Pods onto few nodes saves money, because empty nodes can be shut down by an autoscaler. Choosing between them is one of the most common scheduler tuning decisions, and it is mostly a question of picking the NodeResourcesFit strategy.
9Reserve, Permit, PreBind, Bind, and PostBind: Making the Decision Real
Choosing a node on paper is not the same as placing the Pod there. These last stages close the gap safely.
Reserve: hold it before someone else takes it
As soon as a node is chosen, the scheduler immediately assumes the Pod is on that node in its own cache, even though the real binding has not happened yet. This is so that the very next Pod’s decision already sees the space as taken. Reserve plugins take part in this step. A plugin that tracks something stateful, such as which storage volume was matched to which claim, records its choice during Reserve. Every Reserve plugin also has an Unreserve step, which undoes the hold if anything later fails. Unreserve runs in the reverse order, like unwinding a stack.
When you pick a seat on a booking site, the site holds the seat for a few minutes while you pay. Nobody else can take it during that time. If payment fails, the seat is released. Reserve and Unreserve are the hold and the release.
Permit: the final gatekeeper
The Permit stage is the last chance to stop or delay a Pod before binding. A Permit plugin can answer in three ways. It can approve, which lets the binding continue. It can deny, which sends the Pod back to the queue and triggers Unreserve. Or it can tell the Pod to wait, with a time limit that is at most fifteen minutes. A waiting Pod stays reserved on its node and sits in a holding area until another plugin approves it or the time runs out.
The wait option is what makes gang scheduling possible, which is the rule “start all of these Pods together or none of them.” You will see this in Chapter 15.
PreBind: last preparation
PreBind plugins do work that must finish before the Pod is attached to the node. The best known example is the VolumeBinding plugin. If a Pod needs a storage volume that has to be created or attached on that particular node, the plugin triggers that work here and waits until it is done. If PreBind fails, the whole reservation is undone and the Pod returns to the queue.
Bind and PostBind: the finish line
A Bind plugin performs the actual write that links the Pod to the node. By default, the plugin called DefaultBinder sends a binding request to the API server. Several Bind plugins can exist, and they are tried in order. A plugin that does not want to handle a particular Pod returns the Skip status, and the next plugin gets its turn. This allows, for example, a custom binder for special devices. After a successful bind, PostBind plugins run for information only, such as sending a notice or cleaning up temporary data. They cannot change the result.
Binding involves network calls that may take many milliseconds. If the scheduler waited for each one before looking at the next Pod, it would waste time. Thanks to the assume step, the scheduler can move on at once and let binding finish in the background. If the binding later fails, the assumed hold is forgotten and the Pod is retried.
10The Default Plugin Set at a Glance
A map of which built-in plugin works at which stage.
| Plugin | Extension points it uses | What it cares about |
|---|---|---|
| PrioritySort | QueueSort | Orders the queue by Pod priority, then by arrival time |
| SchedulingGates | PreEnqueue | Keeps gated Pods out of the active queue |
| NodeResourcesFit | PreFilter, Filter, Score | Requested resources versus node capacity |
| NodeName, NodeUnschedulable, NodePorts | Filter (and PreFilter for ports) | Named nodes, drained nodes, host ports |
| NodeAffinity | PreFilter, Filter, PreScore, Score | Node labels and affinity rules |
| TaintToleration | Filter, PreScore, Score | Taints and tolerations |
| InterPodAffinity | PreFilter, Filter, PreScore, Score | Closeness to or distance from other Pods |
| PodTopologySpread | PreFilter, Filter, PreScore, Score | Even spread across zones and nodes |
| NodeResourcesBalancedAllocation | PreScore, Score | Balanced use of CPU and memory |
| ImageLocality | Score | Images already on the node |
| VolumeBinding | PreFilter, Filter, Reserve, PreBind, Score | Storage claims and local volumes |
| VolumeRestrictions, VolumeZone, NodeVolumeLimits | Filter | Exclusive volumes, zone match, attach limits |
| DynamicResources | PreFilter, Filter, Reserve, PreBind | Devices such as GPUs requested through claims |
| DefaultPreemption | PostFilter | Evicting lower priority Pods to make room |
| DefaultBinder | Bind | Writing the final Pod-to-node link |
Look carefully at this table. A plugin like NodeResourcesFit touches three stages. That tells you something about the design: the stages are the vocabulary, and each plugin is a sentence made from those words. When you understand which stages exist, you can read the whole table like a story.
The exact default list and weights can change slightly between Kubernetes versions. For example, an older plugin called SelectorSpread was replaced by default topology spread rules. Always check the documentation for the version you run, and remember that the effective plugin list is also visible in the scheduler’s startup logs when you raise the verbosity.
11Data Flow and Lifecycle: One Pod, Start to Finish
Let us follow a single web-server Pod from creation to running, with the plugins visible at every step.
sequenceDiagram
participant U as User
participant API as API Server
participant Q as Scheduler Queue
participant FW as Plugins
participant K as Kubelet
U->>API: Create Pod with no node
API-->>Q: Watch event for unscheduled Pod
Q->>FW: Pop highest priority Pod
FW->>FW: Filter nodes then score survivors
FW->>FW: Reserve and Permit on best node
FW->>API: Bind Pod to chosen node
API-->>K: Watch event for Pod on this node
K->>API: Report Pod Running
The Pod is created
A Deployment’s ReplicaSet creates a Pod object. It has a name, a list of containers with resource requests, maybe some affinity rules, and an empty node name.
The Pod enters the queue
The scheduler’s watch sees the Pod. PreEnqueue plugins confirm it has no gates, and QueueSort places it according to its priority.
The scheduling cycle begins
The scheduler takes a snapshot of the cluster and pops the Pod. PreFilter plugins prepare shared data, such as the Pod’s total requests.
Filters remove unfit nodes
Out of, say, 200 nodes, 40 are tainted for other work, 25 lack the required zone label, and 70 do not have enough free memory. Sixty-five nodes pass.
Scores rank the survivors
Resource fit prefers emptier nodes, topology spread prefers zones with fewer copies of this service, and image locality prefers nodes that already hold the image. The weighted totals produce one winner.
Reserve and Permit
The scheduler assumes the Pod is on the winner. Reserve plugins record their choices. Permit plugins approve at once, since no group rule applies.
The binding cycle runs
PreBind plugins finish any storage work, then DefaultBinder writes the node name to the API server. PostBind plugins run last.
The kubelet takes over
The kubelet on the chosen node sees a Pod assigned to it, pulls the image if needed, starts the containers, and reports the status. The scheduler’s job for this Pod is finished.
Think of taxi dispatch. A passenger calls. The dispatcher removes taxis that are off duty, too far, or too small (filter). Among the rest, the dispatcher prefers the closest one with the most fuel (score). The dispatcher then marks that taxi as busy (reserve), sends the message (bind), and the driver starts driving (kubelet).
Large e-commerce and streaming platforms that run thousands of services on Kubernetes rely on exactly this flow thousands of times per hour during deployments and autoscaling. During a traffic spike, an autoscaler adds Pods, and the scheduler’s job is to place each of them within moments so that capacity arrives before customers feel the slowdown.
12Priority and Preemption: When Important Pods Push Others Out
Sometimes the cluster is full and an important Pod arrives. Preemption is the fair way to make room.
Every Pod can have a priority, a whole number taken from an object called a PriorityClass. A payment service might have a high priority and an overnight report job a low one. Priority has two effects. First, the queue’s PrioritySort plugin puts higher priority Pods ahead of others. Second, if a high priority Pod cannot be placed anywhere, the DefaultPreemption plugin may remove lower priority Pods to make space. Removing them is called preemption.
flowchart TD
A["Pod cannot fit on any node"] --> B["PostFilter runs DefaultPreemption"]
B --> C{"Does Pod outrank some running Pods?"}
C -->|"No"| X["Pod stays pending"]
C -->|"Yes"| D["For each node pick lowest priority victims"]
D --> E["Choose the node with least damage"]
E --> F["Delete victims and set nominated node"]
F --> G["Pod is scheduled once space frees"]
Find nodes where removal could help
The plugin imagines removing every lower priority Pod from a node and checks whether the new Pod would then pass all filters. Nodes where it still would not fit are skipped.
Choose the smallest sacrifice
On each possible node, it tries to keep as many Pods as it can, removing only the least important ones needed. It also respects PodDisruptionBudgets where possible, which are rules about how many copies of a service may be down at once.
Pick the best node
Among candidates, it prefers the node whose victims have the lowest priorities, then fewer victims, and so on. This limits the damage to the cluster as a whole.
Evict and nominate
The victims are deleted, and the Pod is marked with a nominated node. The Pod does not go straight there. Victims need time to shut down gracefully, so the Pod returns to the queue and is scheduled normally once the space is truly free.
A hospital emergency room is full, and a critical patient arrives. The staff look for the stable patient who can be moved to another ward with the least risk, move that patient, and prepare the bed. They do not empty the whole ward, and they always pick the least harm.
Preemption is powerful and can be surprising. If everything in a cluster is given a high priority, nothing can be preempted and the benefit disappears. If a critical service has a priority that is too high, it may knock out important neighbors. Create a small, deliberate set of priority classes, and document what each one means. A preempting Pod can also set a policy that says “never preempt others,” which suits batch work that should wait politely.
13Configuration: Profiles, Enabling, Disabling, and Tuning Plugins
You change the scheduler’s behavior without touching its source code, by editing a configuration file.
The scheduler reads a configuration object called KubeSchedulerConfiguration when it starts. Inside, you describe one or more profiles. A profile has a name, a list of which plugins are enabled or disabled at each extension point, and optional settings for individual plugins, called plugin arguments. Pods choose a profile by naming it in their schedulerName field. If they do not name one, they use the default profile.
Enable and disable
For each extension point you can switch plugins on, switch them off, or switch off the whole default set and list only your own.
Weights
Each score plugin has a weight. Raising a weight makes that concern matter more when nodes are compared.
Plugin arguments
Many plugins accept their own settings, such as the resource strategy for NodeResourcesFit or default spread rules for PodTopologySpread.
Multi-point
A shortcut that lets you enable or disable a plugin at all of its stages in one line, instead of naming each stage separately.
Percentage of nodes to score
Controls how many feasible nodes the scheduler looks for before it stops searching. Lower means faster but less optimal.
Backoff and parallelism
Initial and maximum backoff times for failed Pods, and how many workers filter and score in parallel, sixteen by default.
Several scheduling personalities in one process
Because a single scheduler can run many profiles, one running program can behave in different ways for different workloads. A common setup defines a default profile that spreads web services widely, and a second profile, perhaps named “bin-packing,” that packs batch jobs tightly. Teams then choose by setting a scheduler name on their Pods. A restriction to remember is that all profiles in one scheduler must use the same QueueSort plugin, since there is only one queue.
| Goal | What to tune | Result |
|---|---|---|
| Save money by packing nodes | Set NodeResourcesFit to the MostAllocated strategy in a dedicated profile | Fewer, fuller nodes, easier for autoscalers to remove empty ones |
| Improve resilience | Keep LeastAllocated and add topology spread rules to Pods | Copies sit on different nodes and zones |
| Faster scheduling in huge clusters | Lower the percentage of nodes to score | Quicker decisions, slightly less optimal choices |
| Start containers faster | Raise the weight of ImageLocality | Pods favor nodes with images already present |
| Remove an unneeded check | Disable the plugin at its extension points | Less work per Pod |
A hotel can run different rules for different guests: a business traveler wants a quiet room near the lift, and a family wants rooms next to each other. The same front desk, using different checklists, serves both. Profiles are those different checklists.
Cloud providers expose this idea as a simple switch. Google Kubernetes Engine, for example, offers an autoscaling profile that favors tighter packing so unused nodes can be removed, and on clusters you manage yourself, the same effect comes from choosing the MostAllocated strategy. Teams that run both online services and large batch jobs on one cluster often use separate profiles for the two kinds of work.
14Writing and Installing Custom Plugins
When the built-in plugins are not enough, you can add your own, and the scheduler treats it exactly like a built-in one.
Custom plugins are written in the Go language, the same language as Kubernetes itself. A plugin is a small component that declares its name and implements one or more of the extension point interfaces. You then build your own copy of the scheduler program, called an out-of-tree build because the plugin lives outside the main Kubernetes source tree. In that build, you register the plugin with the framework. Finally you list it in the scheduler configuration so that it is switched on at the right stages. Because the plugin is compiled into the scheduler, it runs in the same process and has no network cost.
Decide which stage fits your rule
A yes-or-no rule belongs in Filter. A preference belongs in Score. A “wait for friends” rule belongs in Permit. Heavy shared preparation belongs in PreFilter or PreScore.
Write the plugin logic
Implement the interface for that stage. Use the per-cycle storage, described below, to share data between stages. Return the right status code.
Build a custom scheduler binary
Compile the standard scheduler with your plugin registered alongside the built-in ones.
Deploy it as a second scheduler or replace the default
Most teams run it as an extra scheduler with its own profile name, so only the Pods that ask for it are affected. This keeps risk low.
Select it from Pods
Pods choose the new scheduler through their scheduler name. Other workloads keep using the default scheduler.
CycleState: a notebook for one scheduling attempt
The scheduling cycle is split into many stages, but plugins often need to pass information from one stage to another. For this, the framework gives every attempt a small storage area called CycleState. A PreFilter plugin writes its precomputed result into it, and the Filter plugin for the same topic reads it later. The notebook is thrown away when the attempt ends. Because Filter runs on many nodes in parallel, whatever is stored must be safe to read from many workers at once.
Think of a sticky note on a patient’s file during a single hospital visit. The nurse writes “allergic to penicillin” at check-in. The doctor reads it later in the same visit. When the visit is over, the sticky note is thrown away and a new one is made for the next visit.
Do not start from zero: the scheduler-plugins project
A community project under the Kubernetes organization, called scheduler-plugins, collects ready-made out-of-tree plugins. It includes a Coscheduling plugin for gang scheduling, a CapacityScheduling plugin for elastic resource quotas, plugins that understand the internal topology of a node such as NUMA zones, and plugins that place Pods based on real usage measurements. Many teams install this project’s scheduler build and simply turn on the plugins they need.
Plugins versus extenders versus a whole new scheduler
| Option | Best for | Main drawback |
|---|---|---|
| Configure built-in plugins | Most needs: spread, pack, weights, affinity | Cannot invent new kinds of rules |
| Custom in-process plugin | New rules that need speed and cluster state | You must build and maintain a scheduler binary |
| Scheduler extender | Quick link to an existing outside service | Slower and limited stages |
| Second full scheduler | Very different workloads such as batch and AI | Two schedulers can compete for the same nodes |
A plugin runs inside the scheduler. A crash, an endless loop, or a slow call in one plugin affects every Pod handled by that process. Keep plugins small, fast, and well tested, and never make slow network calls inside a Filter or Score stage that runs for every node.
15Advanced Scenarios: Gang Scheduling, Topology, Devices, and Smarter Queues
The framework truly shines in the cases that the old scheduler could never handle well.
Gang scheduling with Permit
Some jobs only make sense if all their Pods start together. A distributed machine learning training job with eight workers is useless if only five are running, and worse, those five hold expensive GPUs while waiting for the other three. The classic fix is gang scheduling, also called coscheduling: place all or none. A Coscheduling plugin implements this using the Permit stage.
sequenceDiagram
participant P1 as Worker Pod 1
participant P2 as Worker Pod 2
participant P3 as Worker Pod 3
participant PM as Permit plugin
P1->>PM: Reserved on node A, ask permission
PM-->>P1: Wait, group has 1 of 3
P2->>PM: Reserved on node B, ask permission
PM-->>P2: Wait, group has 2 of 3
P3->>PM: Reserved on node C, ask permission
PM-->>P3: Allow, group complete
PM-->>P1: Allow waiting Pod
PM-->>P2: Allow waiting Pod
Each Pod in the group passes filtering and scoring on its own and gets reserved on a node. At Permit, the plugin counts how many group members are reserved. Until the count reaches the required number, each Pod waits. When the last one arrives, all waiting Pods are approved together and their binding cycles proceed at the same moment. If the timeout runs out first, the waiting Pods are rejected, their holds are undone, and the capacity returns to the cluster.
A group of friends is booking a table. The restaurant holds each seat as friends arrive, but does not serve anyone until the whole party is present. If half the group never comes, the restaurant releases the seats after a while.
Topology awareness
Modern servers have several processors, and memory attached to each. A Pod that uses a fast network card and a GPU runs best if all of its resources sit close together inside the machine. Plugins in the scheduler-plugins project read information about each node’s internal layout and prefer nodes where the Pod’s resources align. In the same way, the built-in topology spread plugin works at a larger scale, keeping copies of a service apart across zones and racks.
Dynamic resource allocation
Devices like GPUs used to be described only as simple counters. A newer model lets Pods request devices through claims that carry detailed properties, and the DynamicResources plugin takes part in PreFilter, Filter, Reserve, and PreBind to match claims to devices on suitable nodes. This allows richer requests than counting, such as “a GPU with at least this much memory.”
Queueing hints
Earlier you learned that Pods in the unschedulable pool are moved back when an event might help them. In recent Kubernetes versions, plugins can also give finer queueing hints, which are small functions that say whether a specific event really helps a specific Pod. This avoids needless retries. For example, a new node that has the wrong label does not need to wake a Pod that requires a different label. Fewer pointless retries means more time for real scheduling work.
Production example: AI and batch platforms
Platforms for large model training and high performance computing depend on gang scheduling, queue fairness, and device awareness. Projects such as Volcano, Apache YuniKorn, and the scheduler-plugins Coscheduling plugin exist for this purpose, and many organizations that run GPU clusters on Kubernetes use one of them alongside or instead of the default scheduler.
16Performance and Scalability
The scheduling cycle is deliberately one Pod at a time, so every millisecond inside a plugin counts.
Official Kubernetes guidance for a single cluster describes clusters of up to about five thousand nodes and about one hundred and fifty thousand Pods. In such a cluster, the scheduler must examine many nodes for every Pod. Because the scheduling cycle is serial, the time one Pod spends being scheduled limits how many Pods per second the whole cluster can place. Multiply a slow plugin by thousands of nodes and thousands of Pods, and the delay becomes large quickly.
Local snapshot and cache
Plugins read from memory instead of calling the API server. This is the biggest single speed gain.
Parallel filtering and scoring
Node checks run across many workers at once. The default is sixteen workers.
Stop early
The percentage of nodes to score setting lets the scheduler stop searching once it has enough candidates.
Do the heavy work in PreFilter
Compute shared data once per Pod instead of once per node.
Asynchronous binding
The slow network write happens in the background, so the scheduler can start on the next Pod immediately.
Skip unneeded stages
A plugin can return Skip at PreFilter or PreScore when a Pod has no relevant rules, so its Filter and Score stages cost nothing.
The cost of one expensive plugin
Suppose a custom Filter plugin takes one millisecond per node. With a thousand nodes and sixteen parallel workers, that adds roughly sixty milliseconds to every Pod, and during a big rollout of ten thousand Pods, the extra time adds up to many minutes of waiting. A plugin that cached its data in PreFilter and answered in microseconds would remove almost all of that. Always measure a plugin under realistic cluster size before running it in production.
If every guest at a hotel needs the receptionist to phone another office before getting a room, the line stretches out of the door. If the receptionist has a clear, up-to-date list on the screen, the line moves quickly. Slow plugins are those phone calls.
Companies that run very large Kubernetes fleets, including operators of large batch and AI clusters, run scheduling benchmarks before every upgrade and before adding any new plugin. They watch the time per Pod and the number of Pods pending, because those two numbers show scheduler trouble earlier than any user complaint.
Lowering the number of nodes that are scored makes scheduling faster but can yield slightly worse placement, since the best node might not be among those examined. For huge clusters this trade is usually worth it. For small clusters, leave the default and let the scheduler examine everything.
17High Availability and Failure Recovery
If the scheduler stops, new Pods stay Pending. Running Pods keep running, which limits the damage.
The scheduler affects only Pods that are not yet placed. If it stops for a minute, existing applications keep running without a single change. What suffers is anything new: scale-ups, replacements for crashed Pods, and rollouts. For this reason, production clusters run several copies of the scheduler, but only one of them is the active leader at any time. The leader holds a lease, a small object that it must keep renewing. If the leader dies, the lease expires and a standby takes over after a few seconds, as you saw in the controller pattern.
| What fails | What happens | How it recovers |
|---|---|---|
| The active scheduler process | New Pods stay Pending for a short time | A standby wins the lease and rebuilds its cache from the API server |
| A Filter or Score plugin returns an error | That attempt fails for the Pod | The Pod goes to the backoff queue and is retried |
| A bind request fails | The Pod is not placed | Unreserve runs, the assumed hold is forgotten, and the Pod retries |
| A Permit wait times out | The waiting Pod is rejected | Holds are released and the Pod returns to the queue |
| The chosen node dies before the kubelet starts the Pod | The Pod never starts | The node controller and the Pod’s controller replace it, and it is scheduled again |
| The API server is briefly unreachable | Reads and writes fail | Retries with backoff, with no loss since the queue is rebuilt from the API server |
Think of two receptionists at a hotel desk, but only one wears the name badge and takes guests. If the badge holder faints, the colleague picks up the badge and continues. All the bookings are in the shared system, so nothing is lost. The only cost is a few seconds of confusion.
Why a restart is safe
The scheduler keeps no precious private memory. Its queues and cache are rebuilt from the API server, which stores the true state. A restarted scheduler lists all Pods and nodes again, finds the unplaced Pods, and carries on. This is the same level-based thinking used by controllers: always derive the work from the current state, not from remembered events.
When you run a second scheduler for special workloads, it must use a different leader election name than the default scheduler. Otherwise the two compete for the same lease and one of them will sit idle.
Production example: managed control planes
Managed Kubernetes services run multiple scheduler replicas across failure zones as part of the control plane, and upgrade them one at a time. Customers rarely notice the handover, since the standby takes the lease and continues placing Pods.
18Security Considerations
The scheduler decides where every workload runs, so it deserves careful protection.
The scheduler holds a powerful position. It reads Pods, nodes, volumes, and claims, and it writes bindings. If an attacker could control it, they might force sensitive Pods onto a machine they own, or starve important Pods of space. Several layers protect it.
Least-privilege RBAC
The default scheduler uses a narrow built-in role. A second custom scheduler should get its own ServiceAccount with only the permissions its plugins need.
Protect the control plane
Keep scheduler processes on trusted control plane machines, with its credentials and configuration file readable only by the scheduler.
Trust the plugin code
A custom plugin runs with the full power of the scheduler process. Review it, sign the built image, and scan its dependencies.
Guard the placement fields
Use admission policies to stop untrusted users from setting the scheduler name, node name, high priority classes, or powerful tolerations without permission.
Scheduling rules are not security boundaries
Taints, affinity, and node selectors keep workloads apart in normal operation, but they are not strong walls. A user who can create Pods may be able to add a toleration or a node name and cross the line. For true isolation of sensitive workloads, use admission control to enforce which tolerations and selectors are allowed in a namespace, and combine it with node-level protections. Tenants who share a cluster should never rely on scheduling hints alone.
A “staff only” sign on a door tells honest guests where not to go. It does not stop a determined person. A lock, which here means admission control and node isolation, is what actually keeps the door closed.
Allowing every user to create Pods with a very high priority lets one tenant preempt everyone else. Restrict high priority classes with resource quotas scoped to priority classes, so only trusted system workloads can use them.
Production example: multi-tenant platforms
Platform teams in large organizations use admission policies so that application teams cannot choose which nodes or schedulers their Pods use beyond a set of approved profiles. The platform team keeps sensitive node pools reserved for their intended tenants, and the policy engine rejects anything else.
19Monitoring, Logging, Metrics, and Debugging Pending Pods
A quiet scheduler is a healthy one, but only if you can prove it.
The scheduler publishes metrics that Prometheus can collect. The most useful ones answer four simple questions: how many Pods are waiting, how long do they wait, how long does each attempt take, and which plugin is slow.
| Metric | What it tells you | What a bad value looks like |
|---|---|---|
scheduler_pending_pods | Pods waiting, split by queue type | A growing unschedulable count shows real capacity or rule problems |
scheduler_schedule_attempts_total | Attempts by result: scheduled, unschedulable, error | Many errors point to plugin or API trouble |
scheduler_scheduling_attempt_duration_seconds | How long a full attempt takes | Rising time means slower placement |
scheduler_pod_scheduling_duration_seconds | Total time from first queue entry to success | Long tails mean Pods are retried many times |
scheduler_framework_extension_point_duration_seconds | Time spent in each extension point | One stage dominating shows where to look |
scheduler_plugin_execution_duration_seconds | Time spent in each plugin | A single slow plugin is easy to spot |
scheduler_preemption_attempts_total and scheduler_preemption_victims | How often and how many Pods are preempted | Constant preemption means the cluster is undersized or priorities are wrong |
A calm order for debugging a Pending Pod
Read the Pod’s events
A describe command shows a FailedScheduling event listing how many nodes each rule rejected. This is nearly always the answer.
Match the reasons to plugins
“Insufficient cpu” means NodeResourcesFit. “Untolerated taint” means TaintToleration. “Didn’t match Pod’s node affinity” means NodeAffinity. “Didn’t match pod topology spread constraints” means PodTopologySpread.
Check the Pod’s requests and rules
A typing mistake in a label, or a request larger than any node, explains many cases.
Look at the nodes
Check capacity, taints, labels, and whether nodes are cordoned. A cluster autoscaler may also be adding nodes already.
Only then read the scheduler logs
Raise the log level for a short period, since detailed logs are large. Plugin decisions and errors appear there.
If a hotel guest is told “no rooms available,” a good manager asks the front desk why. The answer, “twelve rooms are booked, five are under repair, and three do not allow pets,” is far more useful than a plain no. The FailedScheduling event is that complete answer.
Alert on the number of Pods that have been pending for more than a few minutes, not only on the total pending count. A short burst of pending Pods during a deployment is normal. A Pod stuck for ten minutes is not.
20Advantages, Disadvantages, and Trade-offs
Every design choice has a price. Knowing it helps you decide where to stay on defaults and where to customize.
Advantages
- Extensible without forking: new rules plug in without rewriting the scheduler.
- Fast: plugins run in-process and share the scheduler’s cache.
- Configurable: weights, strategies, and profiles change behavior through a file.
- Clear structure: twelve named stages make every rule easy to place and explain.
- Transparent failures: reasons from filter plugins are counted and shown on the Pod.
- Safe by design: hold, undo, and approve steps prevent half-finished placement.
Disadvantages
- Serial decisions: one Pod at a time limits raw throughput.
- Requests, not reality: poor resource requests lead to poor placement.
- Custom build burden: out-of-tree plugins mean maintaining your own scheduler binary.
- Plugin blast radius: one faulty plugin can disturb every Pod.
- Hard to predict: many weights interacting can surprise even experts.
- One-shot decisions: once placed, a Pod is not moved by the scheduler even if the cluster changes.
| Trade-off | Choice | What you gain | What you give up |
|---|---|---|---|
| Spread versus pack | LeastAllocated or MostAllocated | Resilience, or lower cost | The other benefit |
| Hard versus soft rules | Filter or Score | Guarantees, or flexibility | Possibly stuck Pods, or possibly broken preferences |
| Speed versus quality | Fewer nodes scored | Faster decisions at scale | Slightly less optimal choices |
| One scheduler versus many | Profiles or second scheduler | Tailored behavior | More to operate, possible competition for nodes |
| Preemption on or off | Priority classes | Critical work always fits | Lower priority work can be interrupted |
The missing piece: the scheduler decides once
The scheduler makes its choice at the moment a Pod is created. It does not later move Pods around as conditions change. Over weeks, some nodes may become crowded and others empty. A separate project called the descheduler exists for this purpose. It periodically finds Pods that now violate their rules or that sit in poor places, and evicts them so that the scheduler can place them again with fresh information. The scheduler places, and the descheduler rebalances.
21Design Patterns and Anti-Patterns
A few habits keep scheduling predictable. A few others cause midnight pages.
Patterns worth copying
Honest resource requests
Set requests from measured usage, not guesses. Every other scoring decision depends on them.
Topology spread for resilience
Use spread rules across zones and nodes for services with several copies, instead of long lists of anti-affinity rules.
Small, named priority classes
Three or four classes with clear meanings, such as system, critical, standard, and batch.
Separate profiles for separate goals
Pack batch work and spread online services using different profiles in the same scheduler.
Precompute in PreFilter
Gather shared data once per Pod and reuse it across all nodes.
Gates for admission control
Hold Pods outside the queue until a quota or approval system says they may run.
Anti-patterns to avoid
The mistake
Running Pods with no resource requests at all.
Why it hurts
The scheduler sees such Pods as costing nothing, so it may pile dozens of them on one node, which then runs out of real memory or CPU.
Do this instead
Set realistic requests, and enforce them with namespace defaults and quotas.
The mistake
Making every placement preference a hard rule.
Why it hurts
The more required rules, the fewer nodes pass, and the more often Pods stay Pending, especially during node failures when capacity is already tight.
Do this instead
Use required rules only for true needs, and express wishes as preferred rules that become scores.
The mistake
A custom plugin that calls an outside service for every node.
Why it hurts
Thousands of network calls per Pod slow the whole serial cycle, and an outage of that service stalls all scheduling.
Do this instead
Fetch data periodically in the background into a local cache, and read only that cache during Filter and Score.
The mistake
Running two schedulers that both handle the same Pods or that compete for the same nodes without coordination.
Why it hurts
Each one sees the same free space and may promise it to different Pods, causing failed starts and confusion.
Do this instead
Give each scheduler clearly separate Pods via scheduler name, and where possible separate node pools.
The mistake
Giving everything the highest priority.
Why it hurts
Priority only works as a relative ranking. If all Pods are top rank, ordering and preemption lose their meaning.
Do this instead
Reserve the top classes for a few truly critical workloads and enforce this with quotas.
22Best Practices and Common Mistakes
A short checklist before you tune or extend the scheduler.
Start with the defaults
The default plugin set is well tested and covers most needs. Change one thing at a time and measure the effect.
Prefer configuration over code
Weights, strategies, and profiles solve most problems. Write a custom plugin only when no setting can express your rule.
Keep plugins pure and quick
Filter and Score should only read memory and return fast. Do all slow work elsewhere and cache it.
Always implement the undo
Any plugin that holds something in Reserve must release it in Unreserve, or capacity leaks away.
Use meaningful messages
Return clear reasons with failure statuses. They appear in the Pod’s events and save hours of guessing.
Run multiple replicas with leader election
A standby scheduler cuts any outage to seconds.
Test at realistic scale
A plugin that is fast on ten test nodes can be painfully slow on three thousand. Load test before rollout.
Upgrade carefully
Read the release notes for removed or renamed plugins and changed defaults before moving to a new Kubernetes version.
Mistakes beginners make as cluster users
- Assuming the scheduler sees real usage. It plans from requests. If requests are too low, nodes become overloaded. If too high, nodes look full while idle.
- Misspelling labels in affinity rules. A required rule with a label that exists on no node blocks the Pod forever.
- Forgetting tolerations for tainted nodes. Dedicated GPU or system pools often carry taints, and Pods without matching tolerations never land there.
- Over-using required anti-affinity. Asking for each replica to be on a different node fails once replicas outnumber nodes. Prefer topology spread with a soft setting.
- Ignoring PodDisruptionBudgets. They influence preemption and eviction, and badly set budgets can block maintenance.
- Expecting automatic rebalancing. New nodes do not pull existing Pods across. Use the descheduler or a rollout to rebalance.
When a Pod will not schedule, ask three questions in order: does it fit anywhere by size, are the hard rules too strict, and is something like a taint or a volume zone blocking it. Those three questions explain the overwhelming majority of Pending Pods.
23Real-World and Industry Examples
The framework is not just a lab idea. It shapes how real platforms run their work.
Google and the Kubernetes community: the framework itself
The scheduling framework grew out of the Kubernetes community’s work, with heavy influence from the experience Google gained running Borg. Borg also scored machines and removed lower priority tasks to fit important ones, and Kubernetes carries these ideas forward in plugins that anyone can read and replace.
Cloud providers: Amazon EKS, Google GKE, and Azure AKS
All three run the default scheduler in a managed control plane. Customers influence it through Pod specifications, and some providers offer profiles or autoscaling modes that change packing behavior. Customers who need special behavior run an additional scheduler as an ordinary Deployment.
Volcano and Apache YuniKorn: batch and AI scheduling
Volcano is a project of the Cloud Native Computing Foundation built for batch and high performance computing. Apache YuniKorn offers application-aware scheduling with queues and fair sharing. Both provide gang scheduling and queue policies that go beyond the default scheduler, and many organizations running machine learning clusters adopt one of them.
Alibaba’s Koordinator and other co-location systems
Large companies that run online services and batch jobs on the same machines use scheduler extensions to place batch work in spare capacity without hurting latency-sensitive services. Koordinator, an open source project that came out of Alibaba’s experience, builds such extensions on the scheduling framework.
Cluster autoscalers and Karpenter
The Cluster Autoscaler and Karpenter react to Pods the scheduler could not place. They simulate the scheduler’s own filter rules to decide which new machine would let a pending Pod fit. This is why the clarity of filter rules, such as clear resource requests and node selectors, matters for cost as well as placement.
In each case the same shape appears: a fixed pipeline of stages, and teams adding or tuning plugins at those stages to express what is special about their workloads. The pipeline is the stable part, and the plugins are the creative part.
24Frequently Asked Questions
Short answers to the questions beginners ask most often.
25Summary and Key Takeaways
You began with a hotel front desk and finished with a full picture of how Kubernetes places work. The scheduler is a pipeline with fixed stages, and plugins are the specialists who fill those stages. Filtering removes nodes that cannot work, scoring ranks those that can, and the final stages hold, approve, and write the decision safely. Because even the default scheduler is made of plugins, tuning and extending it follow the same simple rules.
Key Takeaways
- The scheduler picks a node for each unplaced Pod by asking two questions: which nodes are allowed (filtering), and which allowed node is best (scoring).
- The scheduling framework splits that work into extension points from PreEnqueue to PostBind, grouped into a serial scheduling cycle and a parallel binding cycle.
- Plugins are the building blocks. Even the default behavior is made of plugins such as NodeResourcesFit, TaintToleration, and DefaultPreemption, and one plugin can join several stages.
- Hard rules become Filters and soft rules become Scores. Weights, strategies, and profiles in the configuration file let you spread, pack, or favor chosen traits without writing code.
- Reserve, Permit, and the binding stages make placement safe, enabling holds, undo, and group approval, which is the basis of gang scheduling.
- Plan with requests, watch with metrics. Honest resource requests drive good placement, and queue, attempt, and per-plugin timing metrics reveal trouble early.
- Extend with care. Prefer configuration, keep custom plugins fast and well tested, give extra schedulers their own names, and protect placement fields with admission policies and least-privilege access.