Scheduler Framework Plugins in Kubernetes

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.

Everyday analogy

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.

12
extension points from queue to bind
2
cycles: scheduling and binding
0 to 100
score range for every node

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.

i
Remember this

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.

Everyday analogy

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.

QuestionOld hard-coded rulesScheduler extenderScheduling framework plugins
How do you add a rule?Edit and rebuild the schedulerRun a separate web serviceRegister a plugin at an extension point
SpeedFastSlower, network round tripFast, runs in the same process
Access to scheduler stateFull but tangledOnly what is sent in the requestShared cache and per-cycle state
Where can it take part?Two placesA few placesEvery stage of the pipeline
Turn a rule offChange the codeRemove the extenderChange 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.

i
Good to know

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.

Unit

Pod

The smallest deployable unit in Kubernetes. One or more containers that always run together on the same node.

Machine

Node

A machine in the cluster that has CPU, memory, and other capacity to offer to Pods.

Request

Resource requests

The amount of CPU and memory a Pod says it needs. The scheduler plans using these numbers, not the live usage.

Rule

Affinity and anti-affinity

Preferences or requirements about being placed near or far from certain nodes or other Pods.

Rule

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.”

Rank

Priority

A number on a Pod that says how important it is. Higher priority Pods are scheduled first and may push out lower ones.

Stage

Extension point

A named stage of the scheduling pipeline where plugins can attach, such as Filter or Score.

Settings

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.

Everyday analogy

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"]
    
Figure 1. The scheduler watches the API server, thinks locally, and writes only the final decision back.
1

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.

2

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.

3

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.

4

The framework runs the plugins

The framework walks the Pod through its stages, calling each registered plugin and combining the answers.

5

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.

Everyday analogy

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.

i
Production example

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"]
    
Figure 2. The three waiting areas and the paths between them.
Line 1

activeQ

Pods that are ready to be tried right now, sorted so the most important Pod is at the front.

Line 2

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.

Line 3

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.

Door

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.

Everyday analogy

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.

i
Production example

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
    
Figure 3. Every extension point, from entering the queue to finishing the bind.
Extension pointPlain-words jobTypical answer
PreEnqueueDecide whether the Pod may enter the active queue at allAllow or hold back
QueueSortDecide the order of Pods in the queueWhich of two Pods goes first
PreFilterPrepare information once per Pod and reject early if the Pod is hopelessSuccess, fail, or a shorter node list
FilterSay yes or no for each nodeFits or does not fit
PostFilterTry to rescue a Pod when no node fitsNominate a node after making room
PreScorePrepare information once before scoringSuccess or skip
ScoreGive each remaining node a number0 to 100 per node
NormalizeScoreRescale a plugin’s numbers into the allowed rangeAdjusted scores
ReserveHold the chosen resources so nobody else takes themSuccess or fail, with an undo step
PermitFinal approval before bindingApprove, deny, or wait
PreBindDo last preparation work before bindingSuccess or fail
BindWrite the Pod-to-node linkSuccess, fail, or skip
PostBindClean up or send notices after successNothing, informational
Everyday analogy

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.

i
One plugin, many stages

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.

Everyday analogy

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.

Filter

NodeResourcesFit

Is there enough unreserved CPU, memory, and other resources on the node for this Pod’s requests?

Filter

NodeName and NodeUnschedulable

If the Pod names a node, only that node passes. Nodes marked as unschedulable, such as those being drained, are rejected.

Filter

NodeAffinity

Does the node’s labels satisfy the Pod’s required node selector and required affinity rules?

Filter

TaintToleration

Does the Pod tolerate every hard taint on the node? A node with a taint the Pod cannot tolerate is rejected.

Filter

NodePorts

If the Pod needs a specific host port, is that port free on the node?

Filter

PodTopologySpread and InterPodAffinity

Would placing the Pod here break a hard spreading rule or a required rule about other Pods?

Filter

Volume plugins

VolumeBinding, VolumeRestrictions, VolumeZone, and NodeVolumeLimits check that storage can attach on this node, in the right zone, within attach limits.

Filter

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.

!
Common trap

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.

Everyday analogy

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

Score

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.

Score

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.

Score

ImageLocality

Prefers nodes that already have the Pod’s container images downloaded, which makes startup faster.

Score

NodeAffinity

Gives a bonus to nodes that match the Pod’s preferred, not required, affinity rules.

Score

TaintToleration

Prefers nodes with fewer soft taints that the Pod does not tolerate.

Score

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.

Score

PodTopologySpread

Rewards nodes in zones or groups that currently hold fewer matching Pods, to keep copies of a service spread out.

Score

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.

TopicAs a hard rule (Filter)As a soft rule (Score)
Node labelsRequired node affinity or node selectorPreferred node affinity
TaintsNoSchedule and NoExecute taintsPreferNoSchedule taints
Other PodsRequired Pod affinity or anti-affinityPreferred Pod affinity or anti-affinity
Spread across zonesTopology spread that refuses to place when skew is too highTopology spread that places anyway but prefers balance
i
Spreading versus packing

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.

Everyday analogy

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.

i
Why the binding cycle runs in the background

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.

PluginExtension points it usesWhat it cares about
PrioritySortQueueSortOrders the queue by Pod priority, then by arrival time
SchedulingGatesPreEnqueueKeeps gated Pods out of the active queue
NodeResourcesFitPreFilter, Filter, ScoreRequested resources versus node capacity
NodeName, NodeUnschedulable, NodePortsFilter (and PreFilter for ports)Named nodes, drained nodes, host ports
NodeAffinityPreFilter, Filter, PreScore, ScoreNode labels and affinity rules
TaintTolerationFilter, PreScore, ScoreTaints and tolerations
InterPodAffinityPreFilter, Filter, PreScore, ScoreCloseness to or distance from other Pods
PodTopologySpreadPreFilter, Filter, PreScore, ScoreEven spread across zones and nodes
NodeResourcesBalancedAllocationPreScore, ScoreBalanced use of CPU and memory
ImageLocalityScoreImages already on the node
VolumeBindingPreFilter, Filter, Reserve, PreBind, ScoreStorage claims and local volumes
VolumeRestrictions, VolumeZone, NodeVolumeLimitsFilterExclusive volumes, zone match, attach limits
DynamicResourcesPreFilter, Filter, Reserve, PreBindDevices such as GPUs requested through claims
DefaultPreemptionPostFilterEvicting lower priority Pods to make room
DefaultBinderBindWriting 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.

i
Good to know

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
    
Figure 4. The life of one Pod from creation to running.
1

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.

2

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.

3

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.

4

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.

5

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.

6

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.

7

The binding cycle runs

PreBind plugins finish any storage work, then DefaultBinder writes the node name to the API server. PostBind plugins run last.

8

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.

Everyday analogy

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).

i
Production example

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"]
    
Figure 5. How the default preemption plugin makes room.
1

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.

2

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.

3

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.

4

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.

Everyday analogy

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.

!
Common trap

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.

Setting

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.

Setting

Weights

Each score plugin has a weight. Raising a weight makes that concern matter more when nodes are compared.

Setting

Plugin arguments

Many plugins accept their own settings, such as the resource strategy for NodeResourcesFit or default spread rules for PodTopologySpread.

Setting

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.

Setting

Percentage of nodes to score

Controls how many feasible nodes the scheduler looks for before it stops searching. Lower means faster but less optimal.

Setting

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.

GoalWhat to tuneResult
Save money by packing nodesSet NodeResourcesFit to the MostAllocated strategy in a dedicated profileFewer, fuller nodes, easier for autoscalers to remove empty ones
Improve resilienceKeep LeastAllocated and add topology spread rules to PodsCopies sit on different nodes and zones
Faster scheduling in huge clustersLower the percentage of nodes to scoreQuicker decisions, slightly less optimal choices
Start containers fasterRaise the weight of ImageLocalityPods favor nodes with images already present
Remove an unneeded checkDisable the plugin at its extension pointsLess work per Pod
Everyday analogy

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.

i
Production example

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.

1

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.

2

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.

3

Build a custom scheduler binary

Compile the standard scheduler with your plugin registered alongside the built-in ones.

4

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.

5

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.

Everyday analogy

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

OptionBest forMain drawback
Configure built-in pluginsMost needs: spread, pack, weights, affinityCannot invent new kinds of rules
Custom in-process pluginNew rules that need speed and cluster stateYou must build and maintain a scheduler binary
Scheduler extenderQuick link to an existing outside serviceSlower and limited stages
Second full schedulerVery different workloads such as batch and AITwo schedulers can compete for the same nodes
!
Common trap

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
    
Figure 6. A Permit plugin holds early Pods until the whole group has a place.

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.

Everyday analogy

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.

Technique

Local snapshot and cache

Plugins read from memory instead of calling the API server. This is the biggest single speed gain.

Technique

Parallel filtering and scoring

Node checks run across many workers at once. The default is sixteen workers.

Technique

Stop early

The percentage of nodes to score setting lets the scheduler stop searching once it has enough candidates.

Technique

Do the heavy work in PreFilter

Compute shared data once per Pod instead of once per node.

Technique

Asynchronous binding

The slow network write happens in the background, so the scheduler can start on the next Pod immediately.

Technique

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.

Everyday analogy

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.

i
Production example

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.

i
A note on quality versus speed

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 failsWhat happensHow it recovers
The active scheduler processNew Pods stay Pending for a short timeA standby wins the lease and rebuilds its cache from the API server
A Filter or Score plugin returns an errorThat attempt fails for the PodThe Pod goes to the backoff queue and is retried
A bind request failsThe Pod is not placedUnreserve runs, the assumed hold is forgotten, and the Pod retries
A Permit wait times outThe waiting Pod is rejectedHolds are released and the Pod returns to the queue
The chosen node dies before the kubelet starts the PodThe Pod never startsThe node controller and the Pod’s controller replace it, and it is scheduled again
The API server is briefly unreachableReads and writes failRetries with backoff, with no loss since the queue is rebuilt from the API server
Everyday analogy

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.

!
Common trap

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.

Identity

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.

Isolation

Protect the control plane

Keep scheduler processes on trusted control plane machines, with its credentials and configuration file readable only by the scheduler.

Supply chain

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.

Policy

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.

Everyday analogy

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.

!
Common trap

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.

MetricWhat it tells youWhat a bad value looks like
scheduler_pending_podsPods waiting, split by queue typeA growing unschedulable count shows real capacity or rule problems
scheduler_schedule_attempts_totalAttempts by result: scheduled, unschedulable, errorMany errors point to plugin or API trouble
scheduler_scheduling_attempt_duration_secondsHow long a full attempt takesRising time means slower placement
scheduler_pod_scheduling_duration_secondsTotal time from first queue entry to successLong tails mean Pods are retried many times
scheduler_framework_extension_point_duration_secondsTime spent in each extension pointOne stage dominating shows where to look
scheduler_plugin_execution_duration_secondsTime spent in each pluginA single slow plugin is easy to spot
scheduler_preemption_attempts_total and scheduler_preemption_victimsHow often and how many Pods are preemptedConstant preemption means the cluster is undersized or priorities are wrong

A calm order for debugging a Pending Pod

1

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.

2

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.

3

Check the Pod’s requests and rules

A typing mistake in a label, or a request larger than any node, explains many cases.

4

Look at the nodes

Check capacity, taints, labels, and whether nodes are cordoned. A cluster autoscaler may also be adding nodes already.

5

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.

Everyday analogy

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.

i
Tip

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-offChoiceWhat you gainWhat you give up
Spread versus packLeastAllocated or MostAllocatedResilience, or lower costThe other benefit
Hard versus soft rulesFilter or ScoreGuarantees, or flexibilityPossibly stuck Pods, or possibly broken preferences
Speed versus qualityFewer nodes scoredFaster decisions at scaleSlightly less optimal choices
One scheduler versus manyProfiles or second schedulerTailored behaviorMore to operate, possible competition for nodes
Preemption on or offPriority classesCritical work always fitsLower 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.

The scheduler makes one good decision at the right moment. Everything after that is somebody else’s job.

21Design Patterns and Anti-Patterns

A few habits keep scheduling predictable. A few others cause midnight pages.

Patterns worth copying

Pattern

Honest resource requests

Set requests from measured usage, not guesses. Every other scoring decision depends on them.

Pattern

Topology spread for resilience

Use spread rules across zones and nodes for services with several copies, instead of long lists of anti-affinity rules.

Pattern

Small, named priority classes

Three or four classes with clear meanings, such as system, critical, standard, and batch.

Pattern

Separate profiles for separate goals

Pack batch work and spread online services using different profiles in the same scheduler.

Pattern

Precompute in PreFilter

Gather shared data once per Pod and reuse it across all nodes.

Pattern

Gates for admission control

Hold Pods outside the queue until a quota or approval system says they may run.

Anti-patterns to avoid

ANTI-PATTERN 01Avoid
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.

ANTI-PATTERN 02Avoid
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.

ANTI-PATTERN 03Avoid
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.

ANTI-PATTERN 04Avoid
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.

ANTI-PATTERN 05Avoid
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.

1

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.

2

Prefer configuration over code

Weights, strategies, and profiles solve most problems. Write a custom plugin only when no setting can express your rule.

3

Keep plugins pure and quick

Filter and Score should only read memory and return fast. Do all slow work elsewhere and cache it.

4

Always implement the undo

Any plugin that holds something in Reserve must release it in Unreserve, or capacity leaks away.

5

Use meaningful messages

Return clear reasons with failure statuses. They appear in the Pod’s events and save hours of guessing.

6

Run multiple replicas with leader election

A standby scheduler cuts any outage to seconds.

7

Test at realistic scale

A plugin that is fast on ten test nodes can be painfully slow on three thousand. Load test before rollout.

8

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.
i
A quick mental check

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.

i
The common thread

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.

Q1What is the difference between a plugin and an extension point?
An extension point is a named stage of the pipeline, such as Filter or Score. A plugin is a piece of logic that attaches to one or more of those stages. The stages are fixed by the framework, while the plugins can be changed.
Q2Does the scheduler start my containers?
No. The scheduler only chooses a node and records that choice. The kubelet on the chosen node starts the containers.
Q3Why is my Pod Pending even though a node has lots of free CPU in my monitoring?
The scheduler uses resource requests, not live usage. The node may be fully reserved by requests while being mostly idle in reality. Check the sum of requests on the node.
Q4Can I run different scheduling behaviors in one cluster?
Yes. You can define several profiles in one scheduler and select them by scheduler name in the Pod, or run an additional scheduler process for special workloads.
Q5What happens if two nodes get the same highest score?
The scheduler chooses between the tied nodes at random. This avoids always piling work on the first node in the list.
Q6Does the scheduler move Pods later if a better node appears?
No. It decides once, when the Pod is unplaced. For later rebalancing, use a tool such as the descheduler or roll out the workload again.
Q7What is the difference between Reserve and Bind?
Reserve holds resources in the scheduler’s own memory so that following decisions see them as taken. Bind writes the final link to the API server so that the node’s kubelet can see the Pod.
Q8Do I need to write Go to change the scheduler?
Not for most needs. Configuration alone can enable or disable plugins, change weights, and change strategies. You only write Go when you need a brand new kind of rule.
Q9What does it mean when a Pod has a nominated node?
Preemption has started clearing space on that node for the Pod. The Pod is expected to land there soon, but it still goes through normal scheduling once the victims have gone.
Q10Is a scheduler extender still supported?
Extenders remain available for linking an external service, but in-process plugins are preferred, because they are faster and can join every stage of the pipeline.
Q11Why does a Pod wait several minutes after I add a new node?
Usually it does not. Node-added events normally move parked Pods back quickly. If a Pod still waits, a different rule, such as a taint or label mismatch on the new node, may still block it.
Q12What stops a gang of Pods from holding resources forever?
The Permit stage has a time limit, at most fifteen minutes. If the group is not complete by then, waiting Pods are rejected, their holds are released, and the capacity returns to the cluster.

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.