Custom Schedulers in Kubernetes
How Kubernetes decides where every Pod runs, and the four different ways you can change that decision, from a small configuration tweak to a brand-new scheduler of your own.
Imagine a busy airport with one person at a desk who assigns every arriving plane to a gate. The person follows a simple routine: find gates big enough for the plane, prefer the one closest to the terminal, and send the plane there. For most days this works perfectly. Then one day the airport starts handling cargo jets that must park next to refrigerated warehouses, or groups of ten planes that must all land together or not at all. The old routine is not wrong, but it was never designed for these cases, so you need to either adjust the routine or hire a different person.
Kubernetes has exactly such a person. It is called the scheduler, and its job is to decide which computer in the cluster should run each new Pod. This guide explains how the default scheduler thinks, why you might want to change how it thinks, and the four ways to do it. You will learn about scheduler profiles, plugins, extenders and fully separate schedulers, and when each is the right tool. Every new term is explained the first time it appears, so you can start from zero.
1Why Placement Matters, and Why Anyone Would Customize It
Before writing anything custom, we need to understand what the scheduler does for you every second, and what it quietly leaves unsolved.
A quick picture of Kubernetes
A cluster is a group of computers that work together. Each computer in the group is called a node. Your application runs inside Pods, and a Pod is the smallest unit Kubernetes manages: one or more containers that always travel together, like two people sharing a taxi. When you ask for ten copies of a web server, Kubernetes creates ten Pods. But creating a Pod only writes a record saying “this should exist.” Someone still has to decide which node will actually run it.
That someone is the scheduler. It is a program that runs in the cluster’s control plane, the part of Kubernetes that makes decisions. It watches for Pods that have no node yet, picks the best node for each, and records the choice. After that, a small program on the chosen node, called the kubelet, starts the containers. The scheduler never starts anything itself. It only chooses.
Why the choice is harder than it looks
Picking a node sounds like picking a seat in a cinema, but there are many things to weigh at once. Does the node have enough free memory and processing power? Does it have the special hardware the Pod needs, such as a graphics card? Is it in the right data centre zone? Does the Pod want to sit next to its database, or far away from its twin so that one failure does not kill both? Do some nodes carry a “keep out” sign that this Pod has not been invited past?
A bad choice is expensive. Packing too many busy Pods onto one node makes them all slow. Spreading Pods too thinly leaves expensive machines nearly empty and your cloud bill high. Putting both copies of a service in one zone turns a small outage into a big one. Companies that run thousands of nodes feel these costs in real money, which is why scheduling is a topic that attracts so much engineering attention.
Think of a hotel receptionist handing out rooms. A good one checks that the room is clean and large enough, then prefers a quiet floor for a guest who asked for quiet, and keeps a family together. A custom scheduler is like giving the receptionist a different rulebook for a special event, such as a wedding where one hundred guests must all be on the same floor, or none of them gets a room.
Where the idea came from
Kubernetes was open-sourced by Google in 2014, shaped by lessons from internal cluster managers the company had run for years. From the start, the design treated the scheduler as just another program that talks to the API server, not as a hidden part of it. That choice is what makes customization possible at all. Anyone can write a program that watches Pods and assigns them to nodes, and Kubernetes will happily accept the result.
In the early years, customization meant either running a second scheduler or using a feature called scheduler policies, where you listed rules in a file. Policies were limited, so the project introduced the scheduling framework, a plugin system with clearly defined hook points. Policies were then removed, and today the default scheduler is itself built entirely out of plugins. That is good news for you: changing its behaviour is mostly a matter of choosing and configuring plugins, and writing a new one when needed.
Single default scheduler
Early Kubernetes shipped one scheduler with a fixed list of checks and preferences.
Second schedulers and extenders
Teams ran additional schedulers or let the default one call out to a web service for extra opinions.
The scheduling framework
A plugin system arrived, with named hook points where code can join the decision.
Profiles and a stable configuration API
One scheduler process can act as several schedulers, each with its own plugin mix. The old policy files were retired.
Batch and AI era
Group scheduling, queueing and hardware-aware placement became everyday needs, leading to projects such as Volcano, YuniKorn and Kueue.
Most clusters never need a custom scheduler. The default one is excellent for ordinary web services and APIs. Custom scheduling pays off for special workloads such as large batch jobs, machine-learning training, or strict cost and compliance rules.
2Core Concepts: The Vocabulary You Need
A short glossary now saves confusion later. Each word comes with a plain-life picture.
| Term | What it means | Everyday picture |
|---|---|---|
| Pod | One or more containers scheduled together as a unit. | A group of friends booking one table. |
| Node | A computer in the cluster that can run Pods. | A restaurant with a certain number of tables. |
| Scheduler | The program that chooses a node for each new Pod. | The host who seats guests. |
| Filtering | Removing nodes that cannot run the Pod at all. | Crossing out restaurants that are closed or too small. |
| Scoring | Giving each remaining node points for how good a fit it is. | Rating the remaining restaurants out of 100. |
| Binding | Recording the final choice on the Pod. | Writing the guest’s name on the reservation book. |
| Resource request | The amount of CPU and memory a Pod says it needs. | “A table for four.” |
| Taint and toleration | A node’s “keep out” sign and a Pod’s permission to ignore it. | A “staff only” door and a staff badge. |
| Affinity | A Pod’s wish to be near (or far from) certain nodes or other Pods. | “Seat us next to the window” or “away from the kitchen.” |
| Priority and preemption | Ranking Pods by importance, and evicting lesser ones to make room. | A VIP guest bumping an ordinary booking. |
| Profile | A named set of plugins and settings inside one scheduler process. | One host who follows a different rulebook for each event. |
| Plugin | A small piece of logic that joins the decision at a hook point. | One checklist item in the host’s routine. |
| Extender | An outside web service the scheduler asks for an opinion. | The host phoning a friend before deciding. |
What “scheduled” really means
A Pod is considered scheduled once its nodeName field is filled in. Before that, the Pod sits in the Pending state with an empty node name. The scheduler’s one job is to turn that empty field into a real node name, by creating a small record called a Binding through the API server. Notice how modest this is. The scheduler does not move containers, pull images or open ports. Every other step is done by other components.
This tiny job description is why replacing the scheduler is realistic. Any program that can read Pods and nodes and write a Binding can play the role. The hard part is not the plumbing; it is making good decisions quickly at scale.
The schedulerName field
Every Pod has a field called schedulerName. If you leave it empty, Kubernetes fills in the value “default-scheduler”. Each scheduler program only handles Pods whose scheduler name matches its own name. This one simple field is how several schedulers can live in the same cluster without fighting: each one ignores Pods meant for the others.
If a Pod names a scheduler that does not exist, nothing handles it. The Pod stays Pending forever, usually with no error event at all, because no scheduler ever looked at it. This is one of the most common mistakes with custom schedulers, and we return to it in the troubleshooting chapter.
A large conference has several registration desks, each with a coloured sign. Your badge says “blue desk,” so you queue at the blue desk, and the red desk staff never touch your paperwork. If your badge says “purple” and no purple desk exists, you wait forever, and nobody tells you why. The schedulerName field is the colour on the badge.
3Architecture: Inside the Default Scheduler
To change something wisely, you need to know its moving parts. Here is the layout of the scheduler you are about to customize.
flowchart LR
API["API server"] -->|watch pods and nodes| INF["Informers and local cache"]
INF --> Q["Scheduling queue: active, backoff, unschedulable"]
Q --> SC["Scheduling cycle: filter and score"]
SC --> BC["Binding cycle: bind"]
BC -->|create binding| API
API --> KL["Kubelet on the chosen node"]
Read the diagram from left to right. The scheduler does not ask the API server for data every time it needs it, because that would be far too slow. Instead it keeps a local copy of all nodes and Pods in memory. The copy is kept fresh by informers, which are small helpers that open a long-lived connection to the API server and receive a message whenever anything changes. This design is what lets one scheduler make decisions in a fraction of a second even in a cluster with thousands of nodes.
Informers and cache
Hold an in-memory snapshot of nodes, Pods, volumes and other objects. Decisions are made against this snapshot, not live data.
Scheduling queue
Holds Pods waiting for a decision. It has three parts: an active queue, a backoff queue and a pool for Pods that currently cannot fit anywhere.
Scheduling cycle
Takes one Pod at a time, removes unfit nodes (filtering) and ranks the rest (scoring). Runs one Pod at a time on purpose, to keep decisions consistent.
Binding cycle
Writes the choice to the API server. Runs in the background so the next Pod’s decision does not wait for the network.
Framework and plugins
Every check and preference is a plugin attached to a named hook point. Your own logic joins in the same way.
Leader election
When several copies run for reliability, only one is active at a time, chosen through a shared lease object.
The three parts of the queue
The queue is smarter than a simple line. The active queue holds Pods that are ready to be tried now, ordered by importance, so a high-priority Pod goes first. When a Pod fails to find a home, it does not hammer the scheduler by retrying instantly. It moves to the unschedulable pool, a parking area for Pods that cannot fit anywhere at the moment. A Pod waits there until something changes that could help it, such as a new node joining, or until a timer expires. Then it moves to the backoff queue, where it waits a short, growing time before it is tried again, much like a person redialling a busy phone number with longer pauses each time.
Everything is a plugin
The default scheduler has no hard-coded rules. Even “does this node have enough memory?” is a plugin, named NodeResourcesFit. The preference for spreading Pods across zones is another plugin, PodTopologySpread. Support for affinity rules, taints, volume limits and image locality are all plugins too. The scheduler core only orchestrates the order in which plugins run and combines their answers. Because of this design, you can switch plugins off, change their weights, or add your own, without touching the core.
Picture a food-court cashier following a checklist: check the order, check the coupon, check the payment, print the receipt. The cashier is just the person who runs the list; each item is separate and replaceable. If the shop wants to add “ask whether they want a loyalty card,” it adds one line to the checklist. The scheduler core is the cashier, and plugins are the lines on the checklist.
Production example: Amazon EKS Fargate
On Amazon EKS, Pods that run on Fargate, the serverless option where AWS manages the machines, are handled by a scheduler with its own name rather than by the standard one. Pods matching a Fargate profile are labelled with that scheduler name, and that separate scheduler places each Pod on its own managed capacity. It is a good real-world reminder that the schedulerName field is not a theory: a major cloud uses it every day to route different kinds of Pods to different decision-makers.
4Internal Working: One Pod’s Journey Through the Scheduler
Follow a single Pod from the moment it is created to the moment a node is chosen, and see where each plugin joins in.
flowchart TD
A["QueueSort: order the waiting Pods"] --> B["PreFilter: prepare data once"]
B --> C["Filter: drop nodes that cannot fit"]
C -->|no node fits| D["PostFilter: try preemption"]
C -->|some nodes fit| E["PreScore: prepare scoring data"]
E --> F["Score: rate each node"]
F --> G["NormalizeScore: scale to 0 to 100"]
G --> H["Reserve: hold the resources"]
H --> I["Permit: approve, deny or wait"]
I --> J["PreBind: prepare, for example volumes"]
J --> K["Bind: write the binding"]
K --> L["PostBind: clean up and notify"]
The diagram shows the extension points, which are the named moments where plugins can join in. The first group (QueueSort through Reserve) belongs to the scheduling cycle; the last group (Permit through PostBind) belongs to the binding cycle. We walk through them below.
Pick the next Pod (QueueSort)
A plugin decides the order of the waiting line. By default, higher-priority Pods go first, and among equals, the older Pod goes first.
Prepare (PreFilter)
Plugins do work that is the same for every node, such as adding up the Pod’s total resource needs, so the node-by-node step stays quick.
Filter
Each node is tested. Is there enough memory? Does the node’s taint block this Pod? Does it match the required labels? A node failing any test is dropped. What remains is the list of feasible nodes.
If nothing fits (PostFilter)
If every node was dropped, plugins get one last chance. The default one tries preemption: it looks for lower-priority Pods that could be evicted to make room.
Score
Every feasible node receives points from every scoring plugin. Each plugin’s score is scaled to a range of 0 to 100, then multiplied by that plugin’s weight, and all the results are added up per node.
Choose and reserve
The highest total wins; a tie is broken at random so that no node is favoured unfairly. The scheduler immediately records the choice in its local cache, assuming it will succeed, so the next Pod sees those resources as taken.
Bind in the background
Permit, PreBind and Bind run next, possibly in parallel with the next Pod’s scheduling cycle. Bind writes the Binding to the API server, and the kubelet on the chosen node takes over.
Two ideas that make it fast
First, optimistic assumption. The scheduler does not wait for the Binding to be confirmed before moving on to the next Pod. It assumes success and updates its own cache at once. If binding later fails, the assumption is undone and the Pod returns to the queue. This keeps the pipeline full instead of idling on network round trips.
Second, partial scoring. In a big cluster, scoring every node for every Pod would be wasteful. A setting named percentageOfNodesToScore lets the scheduler stop looking once it has found enough feasible nodes. The default is adaptive: smaller clusters check nearly everything, while very large ones check only a fraction, with a sensible minimum so choices stay good. The trade-off is clear: a slightly less perfect choice for much higher speed.
A taxi dispatcher gets a request. First they drop every driver who is off shift or too far (filter). Then they rate the rest by distance, rating and fuel (score). They book the best one at once on their screen (assume), and only then send the confirmation message (bind) while already starting on the next request. In a large city they may stop after checking the nearest fifty drivers instead of all five thousand, because the ride would be fine either way.
The Permit stage can make a Pod wait, with a timeout, until a condition is met. This is the hook that makes group scheduling possible: the first Pods of a group wait at Permit until all the others have found nodes too.
5Before You Build Anything: The Built-in Knobs
The cheapest scheduler to maintain is the one you did not write. Kubernetes already gives every Pod a rich set of controls.
| Control | What it does | Use it when |
|---|---|---|
| Resource requests and limits | Tell the scheduler how much CPU and memory the Pod needs. | Always. Honest requests are the foundation of good placement. |
nodeSelector | Requires nodes carrying specific labels. | You need a simple “must run on GPU nodes” rule. |
| Node affinity | A richer version of selectors, with required and preferred rules. | You want soft preferences, like “prefer zone A.” |
| Pod affinity and anti-affinity | Place Pods near or away from other Pods. | Keep a cache near its app, or keep replicas apart. |
| Taints and tolerations | Reserve nodes for chosen Pods only. | Dedicated nodes for a team or special hardware. |
| Topology spread constraints | Spread Pods evenly across zones, racks or nodes. | High availability across failure domains. |
| Priority classes | Rank Pods and allow preemption of lesser ones. | Critical services must win over batch jobs. |
| Scheduling gates | Hold a Pod back from scheduling until you lift the gate. | An external system must prepare something first. |
Topology spread: the underrated tool
Many teams reach for a custom scheduler when what they need is a topology spread constraint. It lets a Pod say, “spread my replicas across zones so that no zone has more than one extra copy compared with any other.” There are three pieces: the maximum skew (how uneven it may get), the topology key (what to spread across, such as zone), and what to do if the rule cannot be met (refuse, or do the best possible). This single feature replaces a surprising number of custom-scheduler ideas.
Scheduling gates: a pause button
A scheduling gate is a marker on a Pod that tells the scheduler “do not look at this one yet.” The Pod stays Pending until some controller removes the gate. This is wonderful for workflows such as “wait until quota is approved” or “wait until the data has been copied to the right region.” It gives you a clean place to add decision logic before scheduling, without owning a whole scheduler.
Before hiring a new receptionist with a new rulebook, first check whether the existing receptionist can already follow special instructions on the booking form: “quiet room,” “ground floor,” “next to my parents.” Those instructions are affinity, selectors and taints. Only when guests need things that cannot be written on the form do you need a new person.
Ask yourself: “Is my need about where a single Pod should go, or about when and together with whom a group of Pods should start?” The first is nearly always solved by the built-in knobs. The second, such as all-or-nothing groups, quota queues and hardware-topology awareness, is where custom scheduling begins to make sense.
The costs people forget
A custom scheduler is software you must run, upgrade, monitor and debug forever. Each Kubernetes release changes scheduler internals a little, so out-of-tree plugins must be rebuilt and retested. Your engineers must learn the scheduling framework. And every new scheduling rule makes placement behaviour harder to predict for the application teams who just want their Pods to start. A mature team treats a custom scheduler as a last resort and a long-term commitment.
6The Four Ways to Customize Scheduling
When the built-in knobs are not enough, you have four options, ordered here from the lightest touch to the heaviest.
1. Configure profiles
Keep the standard scheduler binary. Change which plugins are on, their weights, and their settings through a configuration file. Add extra profiles for different Pod groups.
2. Write plugins
Write new logic in Go using the scheduling framework, compile it into your own scheduler build, and enable it by name.
3. Use an extender
Leave the scheduler alone, but let it call a web service you run to filter or rank nodes, or even to bind.
4. Run a second scheduler
Deploy a completely separate scheduling program, in any language, that handles Pods carrying its name.
flowchart TD
S["Need different placement behaviour"] --> Q1{"Can built in knobs do it?"}
Q1 -->|yes| K["Use affinity, taints, spread, priority"]
Q1 -->|no| Q2{"Only different weights or plugin mix?"}
Q2 -->|yes| P["Scheduler profile via configuration"]
Q2 -->|no| Q3{"Need new logic inside the normal cycle?"}
Q3 -->|yes| Pl["Custom plugin in a built scheduler"]
Q3 -->|no| Q4{"Needs outside data or another language?"}
Q4 -->|yes| E["Extender"]
Q4 -->|no| F["Full second scheduler"]
Read the diagram from the top. It always pushes you toward the lighter option first. If the problem can be solved with a Pod’s own settings, stop there. If you only need to prefer packing over spreading, a profile is enough. If you need a new kind of rule that fits into filtering or scoring, a plugin is the right tool. If the rule needs information from an outside system, an extender might do. And if you have a genuinely different scheduling model, such as a queue-based batch system, a separate scheduler is the honest answer.
| Approach | Effort | Speed | Power | Main risk |
|---|---|---|---|---|
| Profile configuration | Very low | Full speed | Limited to existing plugins | Misjudged weights causing odd placement. |
| Custom plugin | Medium to high | Near full speed | Very high inside the cycle | Rebuild and retest at every Kubernetes upgrade. |
| Extender | Medium | Slower, adds network calls | Medium | Extender outage or delay hurts all scheduling. |
| Second scheduler | Highest | Depends on your design | Total control | Competes with the default for the same nodes. |
Always pick the option that touches the least code you must own. Configuration beats plugins, plugins beat extenders for speed, and extenders beat a whole new scheduler for effort. Reach further down the list only when the lighter option truly cannot express what you need.
7Option One: Scheduler Profiles and Configuration
The lightest and safest way to change scheduling behaviour is to leave the code alone and change the settings.
The configuration file
The scheduler reads a file at startup, passed with a command-line flag, written in a format called KubeSchedulerConfiguration. This file has a stable version (kubescheduler.config.k8s.io/v1), which means the format does not change under you from release to release. Inside it you find global settings, such as how many nodes to score, how long a Pod waits between retries, and how leader election works, followed by a list of profiles.
A profile is a named personality for the scheduler. Each profile has a schedulerName, a list of plugins to turn on or off at each extension point, and optional settings for individual plugins. One running scheduler process can hold several profiles at once. A Pod picks its personality simply by naming it in its own schedulerName field.
A hotel receptionist may follow a “standard guest” routine most days, a “conference group” routine when a company books fifty rooms, and a “late night arrival” routine after midnight. It is the same person with the same skills, but a different checklist and different priorities for each situation. Profiles give the scheduler several checklists in one body.
What you can change
Enable or disable plugins
Switch off a rule you do not want, such as image-locality preference, or switch on a plugin that is shipped but not enabled by default.
Change weights
Make spreading across zones count for three times as much as balanced resource use, so it wins close contests.
Plugin arguments
Pass settings to a plugin, such as the strategy NodeResourcesFit uses to score nodes.
Global tuning
Adjust the percentage of nodes to score, retry delays and the size of internal queues.
Spreading versus packing: the classic switch
The most useful single setting is how the NodeResourcesFit plugin scores nodes. It offers three strategies. LeastAllocated, the default, prefers nodes with the most free room. This spreads Pods out, which is gentle on each node and good for web services. MostAllocated does the opposite: it prefers the fullest node that still fits, which packs Pods tightly. A third strategy, RequestedToCapacityRatio, lets you draw your own curve of how much each fill level is worth.
Why would anyone want packing? Because cloud nodes cost money whether they are busy or not. If Pods are packed onto fewer nodes, the other nodes become empty, and an autoscaler can remove empty nodes entirely. This is exactly the saving that batch and data-processing teams chase. A common design is two profiles in one scheduler: the default profile spreads web services for resilience, and a second profile named, say, “batch-binpack” packs jobs tightly for cost.
Packing a suitcase for a long trip, you fill one bag completely before opening the second, so you pay for fewer bags. Seating guests at a wedding is the opposite: you spread them across tables so that nobody sits alone. LeastAllocated seats guests at the emptiest table; MostAllocated fills one table before opening the next.
One rule about multiple profiles
All profiles inside one scheduler share a single waiting line. Because of that, they must all use the same QueueSort plugin with the same settings. You can vary almost everything else per profile, but not the ordering of the queue. This surprises people the first time a configuration is rejected for it.
Production example: cost-focused batch clusters
Teams running large data-processing and analytics clusters on cloud virtual machines often switch scoring from spreading to packing for their batch workloads. With tightly packed nodes, an autoscaler such as Cluster Autoscaler or Karpenter can release idle machines sooner, which turns a scheduler setting into a visible line on the monthly cloud bill.
On managed services such as Amazon EKS, Google GKE and Azure AKS, the provider runs the control plane, and you normally cannot edit the default scheduler’s configuration file. In that case, run your own additional scheduler as an ordinary Deployment inside the cluster, with the profile you want, and point selected Pods at it. The next chapters show how.
8Option Two: Writing Your Own Scheduler Plugin
When existing plugins cannot express your rule, you add a new plugin. This gives you full power while keeping the proven machinery of the default scheduler.
What a plugin is
A plugin is a small piece of Go code that has a name and implements one or more of the framework’s interfaces, one per extension point. A plugin that implements the Filter interface is asked, “can this Pod run on this node?” A plugin that implements the Score interface is asked, “how good is this node for this Pod, from 0 to 100?” One plugin may take part in several extension points, which is useful when the same piece of knowledge is needed at filtering time and again at scoring time.
You do not replace the scheduler; you build a slightly different copy of it. A small program imports the standard scheduler command, registers your plugin under a name, and starts the scheduler as usual. The result is a custom scheduler binary that behaves exactly like the stock one, plus your plugin. You then enable your plugin by name in a profile.
| Extension point | Question it answers | Typical custom use |
|---|---|---|
| QueueSort | Which waiting Pod goes first? | Order by deadline, tenant fairness or job age. |
| PreFilter | Is anything wrong before looking at nodes? | Reject early if the whole group is incomplete. |
| Filter | Can this node run the Pod? | Licence limits, compliance zones, special hardware. |
| PostFilter | What if no node fits? | Custom preemption or triggering a scale-up. |
| Score | How good is this node? | Cost, carbon, data locality, measured load. |
| Reserve | Hold the choice while binding happens. | Reserve an external resource such as an IP. |
| Permit | Approve, deny, or make it wait? | Wait for all members of a group. |
| PreBind and Bind | Prepare, then record the choice. | Attach a volume, call a cloud API, or do custom binding. |
| PostBind | Clean up and report. | Metrics and notifications. |
Good plugin habits
Keep Filter and Score tiny and fast
They run for every node, for every Pod. A one-millisecond slowdown multiplied by thousands of nodes becomes seconds. Do heavy lifting once, in PreFilter or PreScore.
Never make network calls in the hot path
Read from the scheduler’s local informer caches instead. If you need outside data, refresh it in the background and read the stored copy.
Share data with CycleState
CycleState is a small notebook that lives for one Pod’s scheduling attempt. PreFilter can write a computed value there, and Filter and Score can read it, avoiding repeated work.
Return the right status
“Unschedulable” means this node fails but preemption might help. “UnschedulableAndUnresolvable” means no amount of eviction will help, so the scheduler skips preemption for that node. Choosing correctly saves a lot of wasted work.
Keep scores in range
Scores must land between 0 and 100 after normalization. If your raw numbers use a different scale, supply a normalize step so your plugin does not drown out the others.
Tell the queue what helps
A plugin can declare which cluster events could make a rejected Pod schedulable again, such as a node being added. Without this, parked Pods wait longer or get retried needlessly.
Write the code to run in parallel
The scheduler checks many nodes at the same time, so shared data must be safe for concurrent use.
Plugin ideas that teams actually build
Cost-aware scoring
Prefer cheaper capacity such as reserved or spot instances for tolerant workloads, and keep critical Pods on stable nodes.
Compliance filtering
Refuse to place Pods holding regulated data on nodes outside an approved region or without required certification labels.
Data locality
Prefer nodes that already hold a large dataset, so a job starts quickly without copying terabytes.
Real-usage scoring
Choose nodes by measured recent load rather than only by what Pods requested on paper.
Group admission
Use Permit to hold members of a distributed job until every member has a node.
Topology awareness
Place a Pod on the node whose processor sockets and accelerators sit close together.
A ready-made toolbox: the scheduler-plugins project
The Kubernetes scheduling special interest group maintains an out-of-tree repository named scheduler-plugins. It provides ready-built plugins you can study or use, including coscheduling for all-or-nothing groups, capacity scheduling for elastic quotas across teams, hardware-topology-aware placement, and load-aware scoring plugins known as Trimaran. Even if you never use them, reading their source is the best free education on how real plugins are written.
The price of owning a build
A plugin is compiled against a specific version of Kubernetes libraries. When the cluster moves to a new minor version, you generally have to update the dependencies, rebuild, retest and redeploy your scheduler. Plan for this work in every upgrade cycle, and keep an automated test suite that runs your plugin against a real scheduler, with both fitting and non-fitting Pods, so that the upgrade is a routine task instead of a scary one.
Start with a plugin that only does Score. A scoring plugin can never make a Pod unschedulable, so a bug there produces a worse placement, not a stuck application. Add Filter rules later, after you trust your tests.
9Option Three: Running a Second Scheduler
Sometimes a custom scheduler runs next to the default one. It could be your own plugin build, or a completely separate product.
The steps in plain words
Get a scheduler program
Either your custom build, an upstream scheduler image with your configuration, or a product such as Volcano or YuniKorn.
Give it an identity
Create a service account, which is the scheduler’s login to the cluster.
Grant permissions
Bind the service account to the roles the standard scheduler uses, which allow reading Pods, nodes and volumes and creating Bindings and events. Add access to lease objects for leader election.
Name it uniquely
Choose a schedulerName that no other scheduler uses, and set a distinct leader-election lease name. If two different schedulers share a lease name, they will take turns being switched off.
Deploy it
Run it as a Deployment in the system namespace, typically with two or three replicas so one failure does not stop scheduling. Only one replica is active at a time.
Point Pods at it
Set schedulerName on the Pods that should use it. Then check the Pod’s events: the “Scheduled” event names the scheduler that made the decision.
The danger of two cooks in one kitchen
Here is the problem that surprises most beginners. Two schedulers do not talk to each other. Each keeps its own picture of the cluster. Suppose a node has room for exactly one more Pod. The default scheduler and your custom scheduler both look at the node at nearly the same moment, both see room, and both place a Pod there. The node is now over-committed.
The kubelet on that node is the safety net. When it receives the second Pod, it re-checks the resources and rejects the one that does not fit, marking it as failed with a reason such as “OutOfcpu” or “OutOfmemory”. The controller that owns the Pod, for instance a Deployment, then creates a replacement, which gets scheduled again. The system heals, but you see odd failed Pods and slower startups.
Two ticket agents in different offices sell seats for the same flight using separate lists. Both see one seat left and both sell it. At the gate, the airline discovers the problem and one passenger is turned away. The fix is not smarter agents, it is giving each agent their own set of seats.
How to avoid the clash
- Partition the nodes. Label and taint a group of nodes for the custom scheduler, so the default scheduler never considers them, and the custom one only places Pods there.
- Use a profile instead. If your need is only different scoring, put a second profile in the same scheduler process. Profiles share one cache and one queue, so they cannot race each other.
- Keep Pods separate. Make sure the types of Pods each scheduler handles do not overlap, using schedulerName consistently, ideally enforced by a policy.
Version matching
Keep the scheduler’s version in step with the cluster. A scheduler several versions behind may not understand newer Pod fields, and one far ahead may use API features the server does not offer yet. The usual rule is to match the control plane’s minor version, and to upgrade the extra scheduler right after the cluster.
Production example: Volcano and Apache YuniKorn
Volcano, a project of the Cloud Native Computing Foundation that began at Huawei Cloud, and Apache YuniKorn, which began at Cloudera, are both full alternative schedulers built for batch and data workloads. Teams running Spark, machine-learning training and similar jobs adopt them because they understand whole applications, queues and groups, which the default scheduler, designed around single Pods, does not.
10Option Four: Scheduler Extenders
An extender lets the standard scheduler ask a web service for help, without any recompiling.
How an extender works
An extender is a web service you run. You tell the scheduler its address in the configuration. At chosen moments the scheduler sends it a message over HTTP containing the Pod and the candidate nodes, and the extender answers. It can take part in several steps, each called a verb: filter (remove nodes), prioritize (give scores), preempt (help choose victims) and bind (take over the final binding).
Scores from an extender use a smaller scale, 0 to 10, which the scheduler multiplies by a weight you set and scales into its normal range. This is a small detail, but forgetting it leads to extenders that are either ignored or that dominate every decision.
Useful settings
Ignorable
If marked ignorable, scheduling carries on when the extender is slow or down. Without it, an extender failure blocks scheduling for every Pod it applies to.
Managed resources
List the special resources only your extender understands, such as a fractional accelerator. The scheduler calls the extender only for Pods that request them.
Node cache capable
If your extender keeps its own copy of node data, the scheduler can send just node names instead of full node objects, saving time and bandwidth.
Timeout
A maximum wait for the reply. Set it tight, because the scheduler waits on the extender while a Pod sits in the cycle.
Pros of extenders
- No scheduler rebuild; any language can be used.
- Can consult outside systems, such as a licence server or a vendor’s device manager.
- Easy to deploy, update and roll back separately.
- Good fit for vendor-specific hardware logic.
Cons of extenders
- A network round trip for each call, so scheduling gets slower.
- Cannot share in-memory state with plugins in the scheduling cycle.
- Fewer hook points than the plugin framework.
- Another service that must be highly available.
Since the scheduling framework arrived, plugins are generally preferred for anything speed-sensitive. Extenders remain a sensible choice for narrow cases, especially when a hardware vendor ships a device manager that the scheduler must consult for its special resources.
11Problems That Really Need Something Custom
Here are the situations where teams genuinely outgrow the default scheduler, and the tools the community has built for them.
All-or-nothing: gang scheduling
A distributed training job might need eight workers. Each worker is a Pod. If only five find a node, those five sit idle holding expensive accelerators while waiting for three more, and they may wait forever if another job grabs the remaining capacity. Two such jobs can block each other completely, a situation called a deadlock. Gang scheduling solves this by treating the group as a single unit: either all members get nodes, or none start.
A group booking for a table of eight at a restaurant is useless if the host seats only five of you at one side and offers the other three a table next week. Either the whole party is seated together, or the host should say “come back later.”
Queues and fair sharing
When many teams share one cluster, you want fairness: each team has a guaranteed share, can borrow spare capacity, and gives it back when needed. The default scheduler thinks in single Pods and has no concept of a team’s quota queue. Tools that add queueing decide when a job may start at all, which is a different job from deciding where its Pods land.
Hardware awareness
Modern servers are not flat. Processors have sockets, memory is attached to specific sockets, and accelerators sit on specific buses. A Pod that gets a processor core on one socket but memory from another, or two accelerators that are far apart, runs measurably slower. Topology-aware scheduling picks nodes, and sometimes specific devices, so that closely related resources are physically close together.
Real usage, not just requests
The default scheduler trusts what Pods request, not what they actually use. A node that looks full on paper might be nearly idle because everyone over-requested. Load-aware plugins score nodes using measured usage so they can safely place more work on lightly used machines.
| Project | Kind | Best known for | Works with default scheduler? |
|---|---|---|---|
| scheduler-plugins | Plugin collection | Coscheduling, capacity quotas, topology and load-aware scoring. | It is a build of the scheduler with extra plugins. |
| Volcano | Full batch scheduler | Gang scheduling, queues and job lifecycle for AI and big data. | Runs beside it, for its own Pods. |
| Apache YuniKorn | Full scheduler | Application-aware, hierarchical queues for Spark and similar workloads. | Runs beside it, or replaces it. |
| Kueue | Job queueing | Quota and fair sharing; decides when jobs may start. | Yes, it hands admitted Pods to the standard scheduler. |
| Koordinator | Colocation scheduler | Mixing latency-sensitive and batch work on the same nodes, begun at Alibaba. | Extends it. |
| Descheduler | Rebalancer | Evicts badly placed Pods so they can be placed again. | Yes, it relies on it. |
Kubernetes keeps absorbing the best of these ideas. Scheduling gates arrived to pause Pods, and Dynamic Resource Allocation gives devices such as accelerators a proper request model of their own. Before you build, check whether a newer release already covers your need, because the best custom code is code you can delete.
Production example: scheduling beyond web services
Large companies that run machine-learning training, such as those operating clusters of thousands of accelerators, depend on group scheduling and quota queues because idle accelerators are extremely costly. Open projects like Volcano and Kueue exist because many different organisations hit the same wall with the standard Pod-by-Pod model.
12Data Flow and Lifecycle: What Happens to a Pod That Cannot Fit
The happy path is simple. Real clusters spend a lot of time on the unhappy one, so it deserves its own chapter.
flowchart LR
P["Pod created, nodeName empty"] --> AQ["Active queue"]
AQ --> SCY["Scheduling cycle"]
SCY -->|node found| BIND["Binding"]
BIND --> RUN["Kubelet starts the Pod"]
SCY -->|no node fits| PRE["Preemption tried"]
PRE -->|victims chosen| NOM["Nominated node set, wait"]
NOM --> AQ
PRE -->|nothing to evict| UNS["Unschedulable pool"]
UNS -->|event or timeout| BO["Backoff queue"]
BO --> AQ
Follow the diagram. A new Pod enters the active queue and gets its scheduling cycle. If a node is found, the Pod is bound and the kubelet starts it. If no node fits, the scheduler tries preemption. If it can evict lower-priority Pods, it marks a nominated node on the waiting Pod and sends it back to the queue while the victims shut down. If there is nothing to evict, the Pod goes to the unschedulable pool and waits for a helpful event or a timer, then passes through the backoff queue and tries again.
How preemption really works
Preemption is not instant. The victims are given their normal shutdown grace period to finish their work. During that time, the incoming Pod holds a “nominated node” note so that other Pods do not take the space that is being freed. When the victims are gone, the incoming Pod usually lands on that node, though if something better appears meanwhile, it may go elsewhere. Preemption also tries to respect disruption budgets on a best-effort basis, but it is not guaranteed to honour them.
Reading the failure message
When a Pod cannot be placed, the scheduler writes an event called FailedScheduling, and the message is a short report. A typical message says that none of the nodes are available, followed by a count of the reasons: so many nodes had insufficient CPU, so many carried a taint the Pod does not tolerate, so many did not match the node selector. These counts add up to the total number of nodes. They are your first and best debugging tool, because they tell you which filter is rejecting your Pod.
| Symptom | Likely cause | First thing to check |
|---|---|---|
| Pending, no events at all | The named scheduler does not exist or is not running. | The Pod’s schedulerName and the scheduler’s logs. |
| Insufficient cpu or memory | Requests exceed free capacity on every node. | Requests, node sizes and autoscaler status. |
| Untolerated taint | Nodes are reserved for other workloads. | Tolerations on the Pod. |
| Did not match affinity or selector | Labels are missing or misspelled. | Node labels and the Pod’s rules. |
| Topology spread not satisfied | The skew rule cannot be met with current nodes. | Allowed skew and zone capacity. |
| Pod stays gated | A scheduling gate was never lifted. | The controller responsible for the gate. |
| Placed, then failed with OutOfcpu | Two schedulers raced for the same node. | Partition nodes between schedulers. |
When a Pod’s scheduling seems slow rather than stuck, compare how long it spent waiting in the queue with how long the actual decision took. Queue waiting dominates in most real clusters, and it usually points to capacity or backoff, not to slow plugins.
13Performance and Scalability
A scheduler that makes perfect decisions slowly is worse than one that makes good decisions quickly. Speed is part of correctness here.
Why speed matters
The scheduler handles Pods one at a time in its scheduling cycle. Imagine a shop with one cashier. If each customer takes two seconds and customers arrive faster than one every two seconds, the line grows without limit. The same happens when a big job creates thousands of Pods at once and each decision is slow: the waiting line stretches, startup time grows, and users notice. Total throughput is simply how many decisions per second the cashier can finish.
Kubernetes is officially tested at about five thousand nodes in one cluster, with a default limit of one hundred and ten Pods per node. The default scheduler is tuned to stay responsive at that size, which sets a high bar for anything you add to it.
Nodes times plugins
Every Pod is checked against many nodes, and each check runs every enabled plugin. Adding one slow plugin multiplies across thousands of nodes.
Expensive built-ins
Inter-Pod anti-affinity is the costliest default rule, because each node’s decision depends on the other Pods nearby. Use it sparingly in huge clusters.
API rate limits
The scheduler talks to the API server through a rate limiter. A heavy burst of bindings can hit that limit and slow everything down.
Retries
Pods that cannot fit keep returning to the queue. A large number of such Pods can waste scheduling time unless queue hints keep retries sensible.
Ways to keep it quick
Score fewer nodes
Lower the percentage of nodes to score on very large clusters. Placement quality dips slightly, but speed improves a lot.
Do the work once
Compute shared facts in PreFilter and PreScore, not inside the per-node loop.
Keep per-node work constant
A node check should cost the same however big the cluster is. Avoid scanning every Pod on a node unless you must.
Disable what you do not use
Every enabled plugin has a price. If your cluster has no volumes that need topology checks, you may be paying for nothing.
Test with fake nodes
Tools such as KWOK can pretend to be thousands of nodes cheaply, and the scheduler’s own performance test suite lets you measure throughput before real traffic arrives.
A hotel front desk that must check every room in a 5,000-room hotel before assigning a guest would never finish checking in a coach-load of tourists. A smart desk looks at a few hundred suitable rooms and picks the best of those, accepting that a slightly better room might exist elsewhere. That is the percentage-of-nodes setting.
A plugin that looks fast in a test cluster of ten nodes can be painfully slow at five thousand. Always measure your plugin at the size of your largest real cluster, not at laptop scale.
14High Availability, Reliability and Security
The scheduler sits on the critical path for every new Pod. Treat your custom scheduler with the same care as any core system.
What happens when the scheduler stops
There is a reassuring fact here. If a scheduler goes down, Pods that are already running keep running. The kubelet does not need the scheduler to keep containers alive. What stops is the placement of new Pods, including replacements for crashed ones. They stay Pending until a scheduler returns. This is serious but not catastrophic, which is why a failing custom scheduler should never take down the cluster by itself.
Staying available
Run two or three copies of your scheduler with leader election switched on. The copies compete for a small lease object stored in the API server. The winner is the active scheduler and keeps renewing the lease every few seconds. If it dies, the others notice that the lease has expired and one of them takes over, usually within roughly a quarter of a minute. Give your scheduler its own lease name so that it never fights with the default one.
| Failure | Effect | Protection |
|---|---|---|
| Scheduler process crashes | New Pods wait; running Pods unaffected. | Multiple replicas with leader election. |
| Bug in a plugin panics | Crash loop on every replica, so no scheduling. | Canary schedulerName, tests, easy rollback. |
| Bad configuration file | Scheduler fails to start after a change. | Validate and stage configuration first. |
| Extender down or slow | Scheduling blocks or slows. | Mark ignorable where safe, short timeouts. |
| Wrong scores | Poor placement, hot nodes. | Metrics on node load; review weights. |
| Autoscaler unaware of your rules | New nodes added that your plugin rejects, or none added. | Align the autoscaler with your constraints. |
The autoscaler blind spot
Cluster autoscalers decide whether to add a node by simulating the standard scheduler: “If I add this node, would the waiting Pod fit?” Tools such as Cluster Autoscaler and Karpenter know the built-in rules. They cannot know about your private filter plugin. If your plugin rejects nodes that the simulation says are fine, the autoscaler may keep adding nodes that your scheduler then refuses to use. Always test your custom scheduler together with your autoscaler, and prefer expressing constraints through labels, taints and affinity, which autoscalers already understand.
Security
A scheduler is powerful: it can read every Pod and node, and it decides where workloads run. Treat it carefully.
- Least privilege. Give the service account only the permissions it needs. Start from the standard scheduler’s roles and remove anything your scheduler does not use.
- Control who can pick which scheduler. A Pod chooses its scheduler through
schedulerName, and that field cannot be changed once the Pod exists. Without a guard, any tenant could pick a scheduler with looser rules and dodge quotas or placement policies. Use an admission policy to allow only approved scheduler names per namespace. - Protect the build. A custom scheduler binary has great influence. Build it in a trusted pipeline, scan it, sign the image and pin its version.
- Secure the endpoints. Keep the metrics and health ports behind the cluster network, and secure any extender web service with certificates.
- Remember placement is not a security boundary. Scheduling rules keep Pods apart for performance and availability, but they do not replace proper isolation such as network policy and container sandboxing.
Roll out a new scheduler with opt-in, not opt-out. Let one low-risk application name it in schedulerName first, watch its metrics for a week, then widen the circle. If anything goes wrong, you change one field on one workload and the blast radius stays tiny.
15Monitoring, Logging and Debugging
You cannot improve what you cannot see. The scheduler exports a rich set of signals, and your custom parts should join in.
The metrics worth watching
| Metric (simplified) | What it tells you | Worry when |
|---|---|---|
scheduler_pending_pods | How many Pods wait, split by queue type. | The unschedulable count keeps rising. |
scheduler_schedule_attempts_total | Attempts by result: scheduled, unschedulable or error. | The error share grows. |
scheduler_scheduling_attempt_duration_seconds | How long one scheduling attempt takes. | The slow tail gets longer after a change. |
scheduler_framework_extension_point_duration_seconds | Time spent in each extension point. | Filter or Score dominates. |
scheduler_plugin_execution_duration_seconds | Time per plugin, which finds a slow plugin. | One plugin stands out. |
scheduler_preemption_attempts_total | How often preemption is tried. | It stays high, meaning constant eviction. |
| Pod scheduling latency (end to end) | Time from creation to being placed. | It drifts away from your target. |
Logs and events
The scheduler writes logs at several levels of detail, controlled by a verbosity setting. Normal levels show start-up and errors, and higher levels explain individual decisions, which is invaluable when diagnosing a surprising placement but too noisy to leave on. For each Pod, the events visible through kubectl describe show a “Scheduled” message naming the scheduler and node, or a FailedScheduling message with the per-reason counts we discussed earlier. Collecting these events into your logging system gives you a searchable history of every placement.
A short debugging routine
Read the Pod’s events
Is there a FailedScheduling message? Which reason has the biggest count? No events at all usually means the wrong or missing scheduler name.
Confirm the scheduler is alive
Check that its Pods run and that one replica holds the lease. A scheduler without a leader schedules nothing.
Check the pending queue metrics
Is the problem one Pod or all Pods? A cluster-wide jump points to capacity or a broken plugin.
Raise logging for a short time
Increase verbosity for a few minutes while reproducing the problem, then lower it again.
Rehearse safely
Use a simulator or a staging cluster to replay the Pod against a copy of the node set and watch which plugin says no.
Three simple alerts cover most trouble: Pods stuck Pending for longer than your service target, a sharp rise in scheduling errors, and the 99th percentile scheduling latency moving beyond its normal range. Anything more detailed can be a dashboard rather than a page.
Add your own counters and timers inside your plugin, named clearly. “How many nodes did my cost filter reject today?” is the question you will be asked within a week of launch.
16Design Patterns, Anti-Patterns and Best Practices
Teams who have lived with custom schedulers keep repeating the same lessons. Here they are, so you can skip the expensive part.
Patterns worth copying
Lightest tool first
Pod settings, then profiles, then plugins, then extenders, then a separate scheduler. Move down only with a clear reason.
Score before Filter
A scoring plugin can only make placement worse, never impossible. Filters can strand Pods. Earn trust with scoring first.
Profiles over processes
Several personalities in one scheduler share one cache, so they cannot race each other.
Partition when separate
If you must run two schedulers, give each its own labelled and tainted node group.
Opt-in rollout
Workloads choose the new scheduler by name. Rollback is a single field change.
Queue first, place second
For shared batch clusters, add a quota queue in front of the standard scheduler rather than rewriting placement.
Traps to avoid
What people do
They copy the whole Kubernetes scheduler source into their own repository, edit it in place, and ship that fork.
Why it hurts
Every Kubernetes release changes the scheduler. Merging upstream changes into a heavily edited fork becomes a project of its own, and the fork slowly falls behind on fixes and security patches.
Do this instead
Keep the upstream code untouched and add your logic as plugins registered from a small wrapper program. The wrapper stays tiny and upgrades are routine.
What people do
They let two schedulers place Pods on the same nodes with no coordination.
Why it hurts
Over-commitment races appear. The kubelet rejects some Pods, controllers recreate them, and engineers chase mysterious “OutOfcpu” failures.
Do this instead
Use profiles inside one scheduler, or split the nodes cleanly between schedulers using labels and taints.
What people do
They call an outside database or web service from inside a Filter or Score plugin.
Why it hurts
The call runs for every node and every Pod. One slow reply multiplies into minutes of delay, and an outage there halts all scheduling.
Do this instead
Refresh outside data in the background into a local cache, and let the plugin read from memory.
What people do
They build a scheduler to fix a problem that honest resource requests, topology spread or priorities would have solved.
Why it hurts
The team takes on years of maintenance, and the underlying cause, such as wildly inflated requests, is still there.
Do this instead
Measure real usage, right-size requests, and apply the built-in controls first. Customize only what remains.
Best practices at a glance
Write down the problem first
State, in a sentence, what placement outcome the default scheduler fails to give, and how you will measure success.
Test with realistic clusters
Use fake nodes at production scale, with both fitting and non-fitting Pods, and test your autoscaler together with the scheduler.
Version and automate
Pin Kubernetes versions, rebuild on every minor release, and run the same test suite each time.
Keep explanations visible
Return clear status messages from plugins, so the FailedScheduling event says exactly which rule rejected which nodes.
Have an exit plan
Know how to send all workloads back to the default scheduler within minutes.
Common mistakes and quick fixes
| Mistake | What you see | Fix |
|---|---|---|
Typo in schedulerName | Pod Pending with no events. | Match the exact name in the scheduler’s profile. |
| Missing permissions on the service account | Scheduler logs show forbidden errors. | Bind the standard scheduler roles plus lease access. |
| Same lease name as another scheduler | Schedulers repeatedly lose leadership. | Give each its own lease name. |
| Different QueueSort across profiles | Configuration rejected at start. | Use one QueueSort configuration for all profiles. |
| Scores outside 0 to 100 | One plugin dominates or errors appear. | Add a normalize step. |
| Skipping resource requests | All Pods look equal to the scheduler. | Set honest requests on every container. |
| Version drift | Unknown fields or odd behaviour. | Keep scheduler and control plane on the same minor version. |
Advantages of custom scheduling
- Placement that matches your real business and hardware needs.
- Lower cloud costs through packing and cost-aware choices.
- All-or-nothing starts for distributed jobs.
- Fair sharing of expensive resources between teams.
Disadvantages and trade-offs
- A long-term maintenance cost on every upgrade.
- Harder-to-predict behaviour for application teams.
- Risk of racing schedulers and autoscaler mismatch.
- A new critical system to monitor and secure.
17Real-World and Industry Examples
These stories show the same pattern again and again: a standard scheduler for ordinary work, and something special for the workloads that are different.
Amazon EKS Fargate: a different scheduler for managed capacity
On EKS, Pods that match a Fargate profile are routed to a separate scheduler that places each one on its own managed capacity, while all other Pods continue to use the standard scheduler. This is the cleanest example of schedulerName at work: two decision-makers share one cluster, each owning clearly separate Pods.
Karpenter and Cluster Autoscaler: planning with the scheduler’s rules
Autoscalers on AWS and elsewhere decide which machines to add by simulating how the scheduler would place waiting Pods. They honour node selectors, affinity, taints and spread rules because those are built-in and well understood. Teams that keep their placement rules in these standard fields get automatic scaling for free; teams that hide rules in private plugins must work harder to keep the two systems aligned.
Volcano and YuniKorn: batch and AI at scale
Organisations running Spark analytics and machine-learning training adopt Volcano or Apache YuniKorn for gang scheduling, queues and application-aware placement. The workloads are large groups of cooperating Pods, so a scheduler that sees only one Pod at a time is a poor fit.
Koordinator: mixing online and offline work
Koordinator, which began at Alibaba, targets a very practical goal: using the idle capacity left by latency-sensitive services to run batch work, without hurting the services. It extends scheduling with quality-of-service awareness so that important Pods are protected while spare capacity is not wasted.
Kueue: queueing in front of the standard scheduler
Many platform teams now prefer to keep the default scheduler for placement and add a queue in front of it. Kueue decides when a job may start based on team quotas, and then lets the standard scheduler place its Pods. It is a good example of getting a large part of the benefit without owning a scheduler.
A typical journey at a growing company
A company starts with the default scheduler and web services. Costs grow, so the platform team right-sizes requests and adds topology spread for resilience. Next a data team arrives with big batch jobs, and the platform team adds a packing profile so idle nodes can be released. Machine-learning projects follow, with distributed jobs that deadlock under the default model, so the team installs a queueing tool and a gang-scheduling plugin, rolling them out by opt-in scheduler name. Only a handful of unusual rules, such as data-residency checks, end up in a small custom plugin. By then each layer has a clear job, the default scheduler still handles most Pods, and the custom parts are small, tested and easy to turn off.
18Frequently Asked Questions
schedulerName and confirm that scheduler’s Pods are healthy and one holds the lease.19Summary and Key Takeaways
The Kubernetes scheduler has one clear job: choose a node for every new Pod. It does so through a queue and a two-stage cycle of filtering and scoring, with every rule supplied by a plugin. That plugin design is what makes it customizable. You can change settings through profiles, add your own logic as plugins, ask an outside service through an extender, or run a separate scheduler entirely. The lighter the tool, the lower the lifetime cost, so reach further only when a lighter tool truly cannot express what you need.
Key Takeaways
- The scheduler only chooses. It fills in a Pod’s node name through a Binding. The kubelet does the actual work, and running Pods survive a scheduler outage.
- Try the built-in controls first. Honest requests, affinity, taints, topology spread, priorities and scheduling gates solve most placement needs without any custom code.
- Everything is a plugin. Filtering, scoring, queueing and binding all happen at named extension points, which is why configuration and custom plugins work so well.
- Pick the lightest option. Profiles, then plugins, then extenders, then a second scheduler. Each step down adds maintenance, risk and operational duty.
- Never let schedulers race. Two uncoordinated schedulers on the same nodes cause over-commitment. Use profiles in one process, or split nodes cleanly between schedulers.
- Mind the neighbours. Autoscalers simulate standard rules, so express constraints in standard fields where you can, and test your scheduler together with your autoscaler.
- Operate it like a core system. Use leader election, restrict who may pick a scheduler, watch the pending and latency metrics, roll out by opt-in, and keep a quick way back to the default.