Custom Schedulers in Kubernetes

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.

Everyday analogy

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.

5,000
nodes in a single cluster, the size Kubernetes is officially tested and supported at
110
Pods per node, the default upper limit the kubelet allows
4
main ways to customize placement, covered in this guide
1

Single default scheduler

Early Kubernetes shipped one scheduler with a fixed list of checks and preferences.

2

Second schedulers and extenders

Teams ran additional schedulers or let the default one call out to a web service for extra opinions.

3

The scheduling framework

A plugin system arrived, with named hook points where code can join the decision.

4

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.

5

Batch and AI era

Group scheduling, queueing and hardware-aware placement became everyday needs, leading to projects such as Volcano, YuniKorn and Kueue.

i
Good to know

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.

TermWhat it meansEveryday picture
PodOne or more containers scheduled together as a unit.A group of friends booking one table.
NodeA computer in the cluster that can run Pods.A restaurant with a certain number of tables.
SchedulerThe program that chooses a node for each new Pod.The host who seats guests.
FilteringRemoving nodes that cannot run the Pod at all.Crossing out restaurants that are closed or too small.
ScoringGiving each remaining node points for how good a fit it is.Rating the remaining restaurants out of 100.
BindingRecording the final choice on the Pod.Writing the guest’s name on the reservation book.
Resource requestThe amount of CPU and memory a Pod says it needs.“A table for four.”
Taint and tolerationA node’s “keep out” sign and a Pod’s permission to ignore it.A “staff only” door and a staff badge.
AffinityA 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 preemptionRanking Pods by importance, and evicting lesser ones to make room.A VIP guest bumping an ordinary booking.
ProfileA named set of plugins and settings inside one scheduler process.One host who follows a different rulebook for each event.
PluginA small piece of logic that joins the decision at a hook point.One checklist item in the host’s routine.
ExtenderAn 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.

!
Watch out

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.

Everyday analogy

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"]
    
Figure 1: The main components of the Kubernetes scheduler and how a decision flows through them.

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.

Input

Informers and cache

Hold an in-memory snapshot of nodes, Pods, volumes and other objects. Decisions are made against this snapshot, not live data.

Waiting room

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.

Decision

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.

Commit

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.

Extension

Framework and plugins

Every check and preference is a plugin attached to a named hook point. Your own logic joins in the same way.

Safety

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.

Everyday analogy

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"]
    
Figure 2: The extension points of the scheduling framework, in the order they run for one Pod.

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.

1

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.

2

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.

3

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.

4

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.

5

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.

6

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.

7

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.

Everyday analogy

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.

i
Tip

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.

ControlWhat it doesUse it when
Resource requests and limitsTell the scheduler how much CPU and memory the Pod needs.Always. Honest requests are the foundation of good placement.
nodeSelectorRequires nodes carrying specific labels.You need a simple “must run on GPU nodes” rule.
Node affinityA richer version of selectors, with required and preferred rules.You want soft preferences, like “prefer zone A.”
Pod affinity and anti-affinityPlace Pods near or away from other Pods.Keep a cache near its app, or keep replicas apart.
Taints and tolerationsReserve nodes for chosen Pods only.Dedicated nodes for a team or special hardware.
Topology spread constraintsSpread Pods evenly across zones, racks or nodes.High availability across failure domains.
Priority classesRank Pods and allow preemption of lesser ones.Critical services must win over batch jobs.
Scheduling gatesHold 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.

Everyday analogy

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.

?
A good test

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.

Lightest

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.

Code

2. Write plugins

Write new logic in Go using the scheduling framework, compile it into your own scheduler build, and enable it by name.

Outside call

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.

Heaviest

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"]
    
Figure 3: A simple decision path for choosing the lightest tool that solves the problem.

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.

ApproachEffortSpeedPowerMain risk
Profile configurationVery lowFull speedLimited to existing pluginsMisjudged weights causing odd placement.
Custom pluginMedium to highNear full speedVery high inside the cycleRebuild and retest at every Kubernetes upgrade.
ExtenderMediumSlower, adds network callsMediumExtender outage or delay hurts all scheduling.
Second schedulerHighestDepends on your designTotal controlCompetes with the default for the same nodes.
i
Rule of thumb

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.

Everyday analogy

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

On and off

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.

Importance

Change weights

Make spreading across zones count for three times as much as balanced resource use, so it wins close contests.

Behaviour

Plugin arguments

Pass settings to a plugin, such as the strategy NodeResourcesFit uses to score nodes.

Speed

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.

Everyday analogy

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.

!
Watch out on managed clusters

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 pointQuestion it answersTypical custom use
QueueSortWhich waiting Pod goes first?Order by deadline, tenant fairness or job age.
PreFilterIs anything wrong before looking at nodes?Reject early if the whole group is incomplete.
FilterCan this node run the Pod?Licence limits, compliance zones, special hardware.
PostFilterWhat if no node fits?Custom preemption or triggering a scale-up.
ScoreHow good is this node?Cost, carbon, data locality, measured load.
ReserveHold the choice while binding happens.Reserve an external resource such as an IP.
PermitApprove, deny, or make it wait?Wait for all members of a group.
PreBind and BindPrepare, then record the choice.Attach a volume, call a cloud API, or do custom binding.
PostBindClean up and report.Metrics and notifications.

Good plugin habits

1

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.

2

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.

3

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.

4

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.

5

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.

6

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.

7

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

Money

Cost-aware scoring

Prefer cheaper capacity such as reserved or spot instances for tolerant workloads, and keep critical Pods on stable nodes.

Rules

Compliance filtering

Refuse to place Pods holding regulated data on nodes outside an approved region or without required certification labels.

Data

Data locality

Prefer nodes that already hold a large dataset, so a job starts quickly without copying terabytes.

Load

Real-usage scoring

Choose nodes by measured recent load rather than only by what Pods requested on paper.

Groups

Group admission

Use Permit to hold members of a distributed job until every member has a node.

Hardware

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.

i
Tip

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

1

Get a scheduler program

Either your custom build, an upstream scheduler image with your configuration, or a product such as Volcano or YuniKorn.

2

Give it an identity

Create a service account, which is the scheduler’s login to the cluster.

3

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.

4

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.

5

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.

6

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.

Everyday analogy

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

Safety

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.

Scope

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.

Speed

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.

Limit

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.
i
Where extenders fit today

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.

Everyday analogy

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.

ProjectKindBest known forWorks with default scheduler?
scheduler-pluginsPlugin collectionCoscheduling, capacity quotas, topology and load-aware scoring.It is a build of the scheduler with extra plugins.
VolcanoFull batch schedulerGang scheduling, queues and job lifecycle for AI and big data.Runs beside it, for its own Pods.
Apache YuniKornFull schedulerApplication-aware, hierarchical queues for Spark and similar workloads.Runs beside it, or replaces it.
KueueJob queueingQuota and fair sharing; decides when jobs may start.Yes, it hands admitted Pods to the standard scheduler.
KoordinatorColocation schedulerMixing latency-sensitive and batch work on the same nodes, begun at Alibaba.Extends it.
DeschedulerRebalancerEvicts badly placed Pods so they can be placed again.Yes, it relies on it.
?
A note on newer built-in features

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
    
Figure 4: The lifecycle of a Pod from creation to running, including the retry loop for Pods that do not fit.

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.

SymptomLikely causeFirst thing to check
Pending, no events at allThe named scheduler does not exist or is not running.The Pod’s schedulerName and the scheduler’s logs.
Insufficient cpu or memoryRequests exceed free capacity on every node.Requests, node sizes and autoscaler status.
Untolerated taintNodes are reserved for other workloads.Tolerations on the Pod.
Did not match affinity or selectorLabels are missing or misspelled.Node labels and the Pod’s rules.
Topology spread not satisfiedThe skew rule cannot be met with current nodes.Allowed skew and zone capacity.
Pod stays gatedA scheduling gate was never lifted.The controller responsible for the gate.
Placed, then failed with OutOfcpuTwo schedulers raced for the same node.Partition nodes between schedulers.
i
Tip

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.

Cost driver

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.

Cost driver

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.

Cost driver

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.

Cost driver

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

1

Score fewer nodes

Lower the percentage of nodes to score on very large clusters. Placement quality dips slightly, but speed improves a lot.

2

Do the work once

Compute shared facts in PreFilter and PreScore, not inside the per-node loop.

3

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.

4

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.

5

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.

Everyday analogy

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.

!
Watch out

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.

FailureEffectProtection
Scheduler process crashesNew Pods wait; running Pods unaffected.Multiple replicas with leader election.
Bug in a plugin panicsCrash loop on every replica, so no scheduling.Canary schedulerName, tests, easy rollback.
Bad configuration fileScheduler fails to start after a change.Validate and stage configuration first.
Extender down or slowScheduling blocks or slows.Mark ignorable where safe, short timeouts.
Wrong scoresPoor placement, hot nodes.Metrics on node load; review weights.
Autoscaler unaware of your rulesNew 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.
i
Tip

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 youWorry when
scheduler_pending_podsHow many Pods wait, split by queue type.The unschedulable count keeps rising.
scheduler_schedule_attempts_totalAttempts by result: scheduled, unschedulable or error.The error share grows.
scheduler_scheduling_attempt_duration_secondsHow long one scheduling attempt takes.The slow tail gets longer after a change.
scheduler_framework_extension_point_duration_secondsTime spent in each extension point.Filter or Score dominates.
scheduler_plugin_execution_duration_secondsTime per plugin, which finds a slow plugin.One plugin stands out.
scheduler_preemption_attempts_totalHow 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

1

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.

2

Confirm the scheduler is alive

Check that its Pods run and that one replica holds the lease. A scheduler without a leader schedules nothing.

3

Check the pending queue metrics

Is the problem one Pod or all Pods? A cluster-wide jump points to capacity or a broken plugin.

4

Raise logging for a short time

Increase verbosity for a few minutes while reproducing the problem, then lower it again.

5

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.

?
What to alert on

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.

i
Tip for plugin authors

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

Pattern

Lightest tool first

Pod settings, then profiles, then plugins, then extenders, then a separate scheduler. Move down only with a clear reason.

Pattern

Score before Filter

A scoring plugin can only make placement worse, never impossible. Filters can strand Pods. Earn trust with scoring first.

Pattern

Profiles over processes

Several personalities in one scheduler share one cache, so they cannot race each other.

Pattern

Partition when separate

If you must run two schedulers, give each its own labelled and tainted node group.

Pattern

Opt-in rollout

Workloads choose the new scheduler by name. Rollback is a single field change.

Pattern

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

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

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

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

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

1

Write down the problem first

State, in a sentence, what placement outcome the default scheduler fails to give, and how you will measure success.

2

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.

3

Version and automate

Pin Kubernetes versions, rebuild on every minor release, and run the same test suite each time.

4

Keep explanations visible

Return clear status messages from plugins, so the FailedScheduling event says exactly which rule rejected which nodes.

5

Have an exit plan

Know how to send all workloads back to the default scheduler within minutes.

Common mistakes and quick fixes

MistakeWhat you seeFix
Typo in schedulerNamePod Pending with no events.Match the exact name in the scheduler’s profile.
Missing permissions on the service accountScheduler logs show forbidden errors.Bind the standard scheduler roles plus lease access.
Same lease name as another schedulerSchedulers repeatedly lose leadership.Give each its own lease name.
Different QueueSort across profilesConfiguration rejected at start.Use one QueueSort configuration for all profiles.
Scores outside 0 to 100One plugin dominates or errors appear.Add a normalize step.
Skipping resource requestsAll Pods look equal to the scheduler.Set honest requests on every container.
Version driftUnknown 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.

The best custom scheduler is often the one you decide not to write.

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

Q1Do I need a custom scheduler for my cluster?
Most likely not. If you run ordinary services, the built-in controls (requests, affinity, taints, topology spread, priorities) cover nearly every need. Custom scheduling is for group starts, shared batch queues, special hardware and unusual cost or compliance rules.
Q2What is the difference between a profile and a second scheduler?
A profile is another personality inside one scheduler process. It shares the same cache and queue, so profiles cannot collide. A second scheduler is a separate program with its own cache and queue, which can collide with others if they share nodes.
Q3Can I change the scheduler on Amazon EKS, GKE or AKS?
You normally cannot edit the provider-run default scheduler. You can, however, deploy your own scheduler as a regular workload in the cluster and point chosen Pods to it by name. That works well when you partition nodes properly.
Q4What happens to running Pods if my scheduler goes down?
They keep running. Only new Pods and replacements wait in the Pending state until a scheduler is active again, which is why you run multiple replicas with leader election.
Q5My Pod is Pending and has no events. Why?
Almost always the Pod names a scheduler that is not running or does not exist, so nobody ever looked at it. Check schedulerName and confirm that scheduler’s Pods are healthy and one holds the lease.
Q6Can I change a Pod’s scheduler after it is created?
No. The field is fixed once the Pod exists. You change it on the owning workload, such as a Deployment, so that new Pods are created with the new name.
Q7What is the difference between a plugin and an extender?
A plugin is code compiled into the scheduler and runs in memory at full speed. An extender is an outside web service the scheduler calls over the network. Plugins are faster and more powerful; extenders avoid rebuilding the scheduler.
Q8Why must all profiles share the same QueueSort?
Because they share a single waiting line. Two different orderings cannot both sort the same queue, so the configuration requires one.
Q9How do I pack Pods tightly to save money?
Create a profile that sets the NodeResourcesFit scoring strategy to MostAllocated, and send your cost-sensitive Pods to it. Pair it with an autoscaler that removes empty nodes.
Q10Does preemption always honour Pod disruption budgets?
Only on a best-effort basis. The scheduler prefers victims whose eviction does not violate a budget, but it may still violate one if there is no other way to place a high-priority Pod.
Q11How should I test a new plugin?
Write unit tests for each extension point, then run the whole scheduler against simulated nodes at production scale with a mix of fitting and non-fitting Pods. Include your autoscaler in the test and rehearse the upgrade to the next Kubernetes version.
Q12Are scheduling gates and a custom scheduler the same thing?
No. A gate only holds a Pod back until some controller lifts it; it does not choose a node. It is a light way to insert decision logic before scheduling without owning a scheduler.
Q13Will two schedulers ever place Pods on the same node?
Yes, if you allow it, and they may over-commit it by accident. The kubelet rejects the Pod that does not fit. Prevent the clash by using profiles or by splitting nodes between schedulers.

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.