Multi-Scheduler Setups in Kubernetes
How Kubernetes decides where Pods run, why one scheduling brain is sometimes not enough, and how to run several schedulers or scheduling profiles side by side without Pods fighting over the same node.
Picture a large airport. Every arriving plane needs a gate. A single controller with one rulebook assigns gates: big planes to big gates, nearest gate to the terminal, and so on. This works well until the airport grows. Now cargo flights need special docks, private jets want a quiet terminal, and an airline wants all of its planes parked next to each other so that crews can transfer quickly. One rulebook cannot please everyone, so the airport introduces several controllers, each with their own rules, each handling their own kind of flight. The danger is obvious: two controllers might promise the same gate to two different planes at the same moment.
Kubernetes has exactly the same story. The default scheduler is a good all-round gate controller, but some workloads need different rules. This post explains, from the very beginning, how scheduling works, how to give some Pods a different scheduler, and what can go wrong when more than one scheduler is active. You do not need to know the internals in advance. Each new idea is defined, compared with something from daily life, and then connected to the next one.
1Why One Scheduler Is Sometimes Not Enough
Start with the real-world pressures that push teams toward more than one scheduling policy.
What a scheduler is
A scheduler in Kubernetes is the component that picks a node for every new Pod that does not yet have one. When you create a Pod, it first exists only as a record in the database with an empty node field. The scheduler notices that record, studies the available nodes, picks the best one, and writes the choice back. After that, the kubelet on the chosen node starts the containers. The scheduler never runs containers itself. It only makes the decision.
A scheduler is like a hotel receptionist. Guests arrive with requests: “quiet room,” “near the lift,” “two beds.” The receptionist looks at the room chart and hands over a key. The receptionist does not carry bags or clean rooms. Other staff do that once the key is assigned.
When the default policy falls short
The default scheduler is built to be general. It balances load, respects resource requests, honours affinity and spreads Pods across failure zones. Many teams never need anything else. Trouble starts with specialised needs, which the following cards describe.
All-or-nothing groups
Distributed training or big data jobs need all their Pods to start together. Starting half a group wastes resources while it waits for the rest.
Packing instead of spreading
To shrink a cloud bill, some teams want Pods packed tightly onto few nodes so empty nodes can be removed, the opposite of the default spreading habit.
Hardware awareness
GPUs, special network cards or topology rules may need custom scoring that the default does not know about.
Experiments and safety
Platform teams want to test a new policy on a small set of workloads before trusting it with the whole cluster.
Tenant isolation
Different teams or business units may need different placement rules without touching each other.
Many beginners assume “multiple schedulers” always means installing several scheduler programs. In modern Kubernetes there is often a gentler option: one scheduler program running several named profiles. We will compare these choices carefully, because picking the wrong one is a frequent and costly mistake.
2A Quick Refresher: What the Scheduler Really Does
To reason about several schedulers, first understand how a single one thinks.
Two phases per Pod
For each Pod, the scheduler runs a scheduling cycle followed by a binding cycle. The scheduling cycle is the thinking part: it filters out nodes that cannot host the Pod, then scores the remaining ones, and picks the winner. The binding cycle is the commit part: it tells the API server, “this Pod goes to that node.” The scheduling cycle handles one Pod at a time so that its decisions stay consistent. The binding cycle can run in the background while the scheduler moves on to the next Pod.
Filter, then score
Filtering removes nodes that fail hard requirements, such as not enough free CPU or memory, a taint the Pod does not tolerate, or a required label that is missing. Scoring ranks the survivors on softer preferences, such as spreading evenly or preferring nodes that already have the container image. The highest score wins, and ties are broken at random.
Filtering is like removing hotels from your list that are fully booked or too expensive. Scoring is then comparing the rest by views, breakfast and distance to the beach. You only compare hotels that passed the first test.
The scheduling queue
Unscheduled Pods wait in an internal queue. The queue has more than one part, and understanding it helps later when we talk about problems.
Active queue
Pods that are ready to be tried right now, ordered by priority and arrival time.
Backoff queue
Pods that recently failed and must wait a short, growing delay before another attempt, so that one hard Pod does not hog the scheduler.
Unschedulable set
Pods that cannot fit anywhere at the moment. They sleep until a cluster event, such as a node being added or a Pod being removed, might make them fit.
The scheduler’s local cache
Just like other Kubernetes controllers, the scheduler does not ask the API server about nodes and Pods at every decision. It uses informers to keep its own in-memory picture of nodes, Pods and other relevant objects, updated by watch events. When it picks a node, it immediately records that the Pod now occupies space there, before the official confirmation comes back. This early bookkeeping is called assuming the Pod, and it lets the scheduler make rapid, consistent decisions. It also hints at the heart of the multi-scheduler problem: this cache belongs to one scheduler process only, so a second scheduler has its own separate cache and does not know what the first has just assumed.
Each scheduler program owns a private picture of the cluster. Two programs can therefore hold slightly different views at the same instant. Everything difficult about multi-scheduler setups comes from that one fact.
3How a Pod Chooses Its Scheduler
The mechanism is wonderfully simple: a single name written inside the Pod.
The scheduler name field
Every Pod specification has an optional field called schedulerName. If you leave it empty, Kubernetes fills it with the value default-scheduler. Every scheduler instance is started with a name, and it only picks up Pods whose field matches its own name. Pods that name someone else are ignored. That is the entire routing system.
Think of a mail room with several clerks, each wearing a name badge. Every parcel has a clerk’s name written on it. A clerk only takes parcels carrying their own name and leaves the rest on the table. A parcel with a name nobody wears simply stays on the table forever.
What happens with an unknown name
This last point trips up many beginners. If a Pod asks for a scheduler that does not exist or is not running, nothing complains loudly. The Pod just stays in the Pending state with no node assigned, and the default scheduler also leaves it alone because the name does not match. The usual clue is that the Pod has no scheduling events at all, neither a success nor a failure message, because no scheduler ever looked at it.
Where the name is set
Because workloads like Deployments and Jobs contain a Pod template, the name is set inside that template. Changing it on a Deployment triggers a normal rolling update, and the new Pods will be handled by the new scheduler while old Pods stay where they are. Existing running Pods are never moved or re-scheduled just because their scheduler name is later changed on a controller.
User creates a workload
The Pod template carries a scheduler name, either explicit or the default one filled in by the API server.
The Pod appears unassigned
It is stored with an empty node field, and every scheduler’s informer sees it appear.
Each scheduler checks the name
Only the scheduler whose name matches adds the Pod to its queue. All others skip it immediately.
The owner schedules and binds
The matching scheduler picks a node, writes the binding, and the kubelet takes over.
A Pod that already has a node name written into it skips scheduling altogether, because no scheduler is needed. This is used by some system components, and it is also an easy way to cause trouble, since such Pods are not checked by scheduler filters at all.
4Three Ways to Get Different Scheduling Behaviour
Running a second scheduler program is only one of several options, and often not the best one.
Option A: scheduling profiles in one scheduler
A single scheduler process can run several profiles. A profile is a named set of rules that decides which plugins are active and how they are weighted. Each profile has its own scheduler name, so Pods pick a profile exactly as they would pick a separate scheduler. All profiles share the same process, the same queue and the same cache of the cluster.
Option B: separate scheduler programs
You can run another scheduler as a distinct program, for example as its own Deployment, with its own name, configuration and cache. It can be the standard scheduler built with different settings, a custom build with extra plugins, or an entirely different project written by someone else.
Option C: extend the default scheduler
Instead of replacing the scheduler, you can attach to it. Plugins compiled into a scheduler build add new filtering or scoring steps. Extenders are external web services that the scheduler calls during its work to get extra opinions. These approaches change how a scheduler decides without creating a competing scheduler.
| Aspect | Profiles in one scheduler | Separate scheduler programs | Extenders and plugins |
|---|---|---|---|
| Process count | One | Two or more | One, plus optional web service |
| Shared view of cluster | Yes, one cache and one queue | No, each has its own cache | Yes, inside the same scheduler |
| Risk of two schedulers racing for a node | Very low, decisions are serialised | Real and must be managed | None, still one decision maker |
| Operational effort | Low, one configuration file | High, separate deployment, access rules and monitoring | Medium, build or host extra pieces |
| Freedom to use a totally different algorithm | Limited to available plugins | Complete | Moderate |
| Good first choice? | Yes for most cases | Only when truly necessary | When you need custom logic inside the framework |
Try profiles first. If you need logic that no existing plugin offers, write or adopt a plugin. Reach for a separate scheduler program only when you need a different queueing model, a different release cycle, or hard isolation between teams, because that choice carries the sharpest risks.
flowchart TD
Q["Need different placement behaviour?"] --> A{"Can existing plugins express it?"}
A -- "Yes" --> P["Use a second profile in the same scheduler"]
A -- "No, but logic fits the framework" --> X["Add a custom plugin or an extender"]
A -- "No, needs a different queue or isolation" --> S["Run a separate scheduler with strict partitioning"]
P --> OK["Lowest risk and lowest effort"]
X --> OK2["Moderate effort, still one decision maker"]
S --> OK3["Highest effort, manage race conditions"]
Figure 1 turns the table into a simple path. Notice that the expensive branch is the last one. Many teams jump straight to it because “a second scheduler” sounds like the obvious answer. Walking the path first usually saves months of operational pain.
5The Scheduling Framework and Its Extension Points
The modern scheduler is a pipeline of small plugins. Knowing the stages makes profiles easy to understand.
A pipeline of stages
The scheduler is organised as a series of well-defined stages, called extension points. At each stage, any number of plugins can take part. A profile is essentially a choice of which plugins are switched on at which stages, and with what weights. The most important stages are listed below.
Queue sort
Decides the order of Pods waiting to be scheduled, usually by priority then age. Only one sort plugin can be active in a scheduler.
Pre-filter and filter
Prepares information and then removes nodes that cannot host the Pod.
Post-filter
Runs only when no node fits. This is where preemption lives: deciding whether lower-priority Pods could be evicted to make room.
Score and normalise
Each plugin rates the surviving nodes, ratings are brought to a common scale, and weighted totals pick the winner.
Reserve and permit
Reserve earmarks resources for the Pod. Permit can approve, reject or delay the Pod, which is how group scheduling can hold Pods back until the whole group is ready.
Pre-bind, bind and post-bind
Final preparations such as attaching volumes, then the actual binding to the node, then any cleanup.
Imagine a university admission office with a fixed conveyor: documents check, eligibility check, interview scoring, seat reservation, final letter. A profile is like choosing which examiners sit at each desk and how much weight their marks carry. The conveyor stays the same, the examiners and weights change.
flowchart LR
QS["Queue sort"] --> PF["Pre-filter"]
PF --> F["Filter"]
F --> PO["Post-filter and preemption"]
F --> SC["Score"]
SC --> RS["Reserve"]
RS --> PM["Permit"]
PM --> PB["Pre-bind"]
PB --> B["Bind"]
B --> PS["Post-bind"]
Why this matters for multiple schedulers
Because behaviour is built from plugins and weights, many “I need a different scheduler” requests really mean “I need different weights” or “I need one more plugin.” For example, packing Pods tightly is often a matter of changing which resource scoring strategy is used. Spreading preferences can be tuned by weights. Group scheduling uses the permit stage. Seeing the pipeline makes it much easier to ask whether a profile can do the job before building something heavier.
All profiles inside one scheduler must use the same queue-sort plugin, since there is only one shared queue. If a team needs a completely different ordering of waiting Pods, that is a real reason to consider a separate scheduler program.
6Multiple Profiles in a Single Scheduler
The safest way to run several scheduling personalities: one program, several named profiles.
How profiles are defined
The scheduler reads a configuration file, an object of the kind KubeSchedulerConfiguration, that contains a list of profiles. Each profile has a unique schedulerName. Inside it you enable or disable plugins at each stage and set weights or plugin arguments. A profile named like the default one keeps normal behaviour, and extra profiles can be added beside it with a different name.
What profiles share
All profiles share the scheduler’s cache, informers, queue and leader election. Because a single decision loop handles all Pods one at a time, the scheduler always knows which resources it has just promised to other Pods, regardless of profile. Two profiles can never hand out the same free space twice, which is why this approach avoids the most feared multi-scheduler failure.
One receptionist wearing different hats, for example “family room hat” and “business traveller hat,” follows slightly different rules for each guest type, but works from one master room chart. Two receptionists with two separate charts could both sell the last room.
Typical profile examples
Bin-packing profile for batch work
A profile that favours nodes that are already well used. Batch workloads land together, leaving other nodes empty so that a cluster autoscaler can remove them and save money.
Spread-first profile for web services
A profile that strongly favours even distribution across zones and nodes, increasing resilience for user-facing services.
Experimental profile for safe testing
A copy of the default behaviour with one or two plugins altered. A single team sets their Pods to use it and compares results, while all other workloads continue unchanged.
Advantages of profiles
- No race between schedulers because decisions are serialised
- One deployment, one set of access rules, one set of metrics
- One leader election and one high-availability story
- Easy to add, adjust or remove a profile through a configuration change
- Metrics are labelled by profile, so comparison is simple
Limits of profiles
- All profiles share one queue and one queue-sort rule
- A heavy profile can slow down Pods of the other profiles
- You are limited to the plugins compiled into that scheduler build
- A configuration mistake affects every profile at once
Besides plugins, the configuration also controls how many nodes the scheduler scores in big clusters, and the back-off timing for failed Pods. These settings are scheduler-wide, so changing them for one profile’s benefit changes them for all.
7Running a Second Scheduler as a Separate Program
When profiles are not enough, here is what a clean, safe separate deployment involves.
The ingredients
A second scheduler is just another workload in the cluster, usually a Deployment in the system namespace. It needs an identity, permissions, a configuration, and a unique name. None of these steps is hard, but forgetting any of them gives confusing results.
Choose a unique scheduler name
It must differ from the default name, and from every other scheduler in the cluster. Pods will refer to this exact name.
Create a service account and bind permissions
The scheduler needs to read nodes, Pods, volumes and related objects, and to write bindings and events. Binding it to the same cluster roles that the default scheduler uses is the usual starting point.
Write its configuration
Define its profile name, plugin choices and, importantly, its leader election settings with a lock name different from the default scheduler’s.
Deploy it with health checks
Run one or more replicas with probes, resource requests and a pinned image version matching your cluster’s version family.
Point test Pods at it
Set the scheduler name in a small test workload and verify that events show the new scheduler as the source of the scheduling decision.
Add partitioning and guard rails
Decide which nodes and namespaces belong to which scheduler before real traffic arrives. Chapter 11 covers this in depth.
Version compatibility
A scheduler program is closely tied to the Kubernetes version because it uses the same internal frameworks and API versions. A common safe practice is to run a scheduler build of the same minor version as the control plane, and to upgrade the extra scheduler together with the cluster. A custom scheduler left behind on an old version may miss newer Pod features, for example newer scheduling directives, and quietly place Pods in ways that ignore those fields.
Copying the default scheduler’s configuration without changing the leader election lock name makes the two schedulers compete for one lock. Only one of them will ever become active, and the other silently idles. Pods meant for the idle one remain Pending with no events.
Managed clusters and control plane limits
On many managed Kubernetes services the control plane’s own scheduler cannot be reconfigured by users. This is actually a reason that separate scheduler programs became popular: you cannot change the built-in one, but you can run another beside it. On such platforms a separate scheduler running as a normal workload, or an extension project that provides its own scheduler name, is often the only route to custom policies. Always check your provider’s documentation about supported scheduling customisation.
8Leader Election and High Availability
Many replicas for safety, but exactly one active decision maker for each scheduler.
Why only one replica may decide
If two copies of the same scheduler decided at once, they would assign the same Pod twice or hand out the same free space to different Pods. So replicas of one scheduler take turns using leader election. They compete for a small shared record, a lease stored in the cluster. The holder renews it every few seconds. If the holder crashes and stops renewing, another replica takes over within a short time, typically within seconds up to a quarter of a minute depending on settings.
Leader election is like a single microphone passed around a meeting. Only the person holding the microphone may speak. If they leave the room without passing it on, another person picks it up after a polite pause, so the meeting never ends up with two people shouting orders.
Each scheduler has its own microphone
The shared record is identified by a name and namespace. Two different schedulers, for example the default one and a custom one, must use different record names. Otherwise they fight for the same microphone and only one of them works. Replicas of the same scheduler must use the same name, so they can elect among themselves. In summary: same scheduler, same lock name; different scheduler, different lock name.
flowchart TD
subgraph D["Default scheduler group"]
D1["Replica 1 - leader"]
D2["Replica 2 - standby"]
end
subgraph C["Custom scheduler group"]
C1["Replica 1 - standby"]
C2["Replica 2 - leader"]
end
LD[("Lease: default scheduler lock")] --- D1
LD --- D2
LC[("Lease: custom scheduler lock")] --- C1
LC --- C2
Warm standbys
Standby replicas still run their informers and keep their caches warm. When a standby becomes leader, it starts scheduling immediately without waiting to re-read the entire cluster state. This is the same benefit seen in other controllers: a failover costs a few seconds, not a cold start of minutes.
If a custom scheduler seems alive but never schedules anything, check who holds its lease. If the holder is a crashed or removed Pod, or if the lock name collides with another scheduler, the cause is usually found in minutes.
Failure windows to expect
During a leader change, new Pods wait a few seconds before being placed. Already running Pods are unaffected, because scheduling only matters at the moment a Pod is first placed. A quiet leader failover is therefore not an outage, though a long-lasting inability to elect a leader is, because new Pods for that scheduler would pile up in Pending.
9End-to-End Data Flow with Two Schedulers
Follow two Pods, each owned by a different scheduler, from creation to running.
The shared stage
Both schedulers watch the same API server using informers. When any new Pod is created, both of them see the event. Each one checks the scheduler name in the Pod. The matching scheduler adds it to its queue, while the other ignores it. After that, each works through its own queue independently.
sequenceDiagram
participant U as User
participant A as API Server
participant S1 as Default Scheduler
participant S2 as Custom Scheduler
participant K as Kubelet
U->>A: Create Pod A (default name)
U->>A: Create Pod B (custom name)
A-->>S1: Watch event for Pod A and Pod B
A-->>S2: Watch event for Pod A and Pod B
Note over S1: Pod A matches, Pod B ignored
Note over S2: Pod B matches, Pod A ignored
S1->>S1: Filter, score, assume Pod A on node 1
S2->>S2: Filter, score, assume Pod B on node 2
S1->>A: Bind Pod A to node 1
S2->>A: Bind Pod B to node 2
A-->>K: Pod A and Pod B assigned to their nodes
K->>K: Start containers
Reading the flow step by step
The user creates Pod A with the default name and Pod B with the custom name. The API server stores both and the watch events reach both schedulers. Each scheduler runs its own filter and score steps for the Pod it owns and records, in its private cache, that the chosen node now has less free space. Each then sends a binding request to the API server. The API server accepts the bindings, and the kubelet on each chosen node, which is watching for Pods assigned to it, starts the containers.
Where the schedulers learn about each other
Look closely at the middle of the diagram. When the default scheduler assumes Pod A on node 1, the custom scheduler does not hear about it at once. It only learns after the binding is stored and the resulting watch event arrives. That delay is typically tiny, from milliseconds to perhaps a few hundred milliseconds under load, but it is not zero. During that window, the custom scheduler might still believe node 1 has the space that Pod A has just claimed. The next chapter shows what happens when both of them want the same space.
10The Core Danger: Two Schedulers, One Node
The race condition that makes experienced operators nervous about separate schedulers.
The race in plain words
Suppose a node has room for exactly one more Pod. At almost the same moment, the default scheduler decides to place Pod A there, and the custom scheduler decides to place Pod B there. Both read the same free space from their caches, both think it is available, and both send a binding. The API server does not check resource arithmetic when storing a binding. It only checks that the Pod is not already bound. So both bindings are accepted, and the node is now promised more than it can hold.
Two ticket sellers each have their own paper copy of the seat chart for the last seat in a cinema. Both look at the chart, both see the seat as free, and both sell it. Neither did anything wrong by their own chart. The problem is that the charts are separate.
Who catches the mistake
The safety net is the kubelet on the node. When it receives a newly assigned Pod, it performs its own admission check against the real resources of the node. If the node cannot actually accommodate the Pod, the kubelet rejects it and marks it as failed, with a reason such as insufficient CPU or memory. A controller above the Pod, for example a Deployment’s ReplicaSet, then creates a replacement, which goes through scheduling again. So the system does heal, but it pays with a failed Pod, a short delay and some noisy events.
sequenceDiagram
participant S1 as Scheduler 1
participant S2 as Scheduler 2
participant A as API Server
participant K as Kubelet on node X
Note over S1,S2: Both caches show room for one Pod on node X
S1->>A: Bind Pod A to node X
S2->>A: Bind Pod B to node X
A-->>K: Pod A assigned
A-->>K: Pod B assigned
K->>K: Admit Pod A, space used
K->>K: Check Pod B, no space left
K-->>A: Mark Pod B failed, out of resources
Note over A: Controller creates a replacement Pod
Why it is worse than it sounds
A single rejected Pod seems minor. The danger grows in busy clusters. If batch jobs submit thousands of Pods at once and two schedulers compete for the same nodes, conflicts become common. Failed Pods accumulate, replacement Pods trigger more scheduling load, and operators see a flood of failures that look like node problems but are really coordination problems. Some Pod types, for example those from a Job with a low retry limit, may even cause the whole Job to be marked failed because too many of its Pods were rejected.
Free CPU and memory are not the only shared resources. Two schedulers can also double-book a scarce device, such as a GPU, a host port, or a volume that can only attach to one node. Some of these conflicts are caught later or not caught at all, so the damage may be harder to spot than a simple out-of-resource rejection.
11Partitioning: Keeping Schedulers Out of Each Other’s Way
The cleanest way to avoid the race is to make sure two schedulers never compete for the same nodes.
The big idea
If each scheduler only ever considers its own set of nodes, they can never double-book the same space. This is partitioning. It turns a risky shared-resource problem into two independent, safe problems. There are several tools that help, and they can be combined.
Node labels and selectors
Label nodes by pool, and make the Pods of each scheduler select their own pool. Pods of the other scheduler never choose those nodes because they do not select the label.
Taints and tolerations
Taint a pool so that only Pods carrying the matching toleration may land there. This guards the pool against Pods that forgot to select it.
Plugin-level node filtering
Configure the custom scheduler’s own filter plugins to consider only nodes with certain labels, so partitioning is enforced by the scheduler, not just by each Pod’s request.
Admission policies
Cluster rules that reject or modify Pods automatically, for example enforcing that Pods in a namespace use the right scheduler name and carry the right pool selector.
flowchart LR
subgraph PoolA["Node pool A: general workloads"]
N1["Node 1"]
N2["Node 2"]
end
subgraph PoolB["Node pool B: batch and GPU workloads"]
N3["Node 3"]
N4["Node 4"]
end
S1["Default scheduler"] --> PoolA
S2["Custom scheduler"] --> PoolB
Layered defence works best
Relying on only one tool is fragile. For example, node labels alone depend on every workload author remembering the right selector. Adding a taint on the batch pool protects it from stray default-scheduler Pods. Adding an admission rule that forces the right scheduler name for the batch namespace removes human error. With all three, mistakes are caught at the earliest and cheapest place.
Benefits of partitioning
- Eliminates cross-scheduler double-booking by construction
- Makes capacity planning per team or workload type easy
- Allows different node hardware for different policies
- Simplifies debugging, since each Pod’s possible nodes are limited
Costs of partitioning
- Lower overall utilisation, since spare space in one pool cannot help the other
- More node pools to manage and autoscale separately
- Labels, taints and policies must be kept consistent over time
Remember that system Pods and per-node agents also consume node resources. Even in a partitioned world, they come from the default scheduler or from direct assignment. Keep room for them in each pool by reserving resources, so that partitioning does not push nodes into pressure.
12Priorities and Preemption Across Schedulers
Priority is a cluster-wide idea, but each scheduler acts on it independently. That has consequences.
Priority classes
A priority class gives a Pod a number that represents how important it is. When the scheduler cannot place a high-priority Pod anywhere, it may decide to evict lower-priority Pods to make room. This act is called preemption. The evicted Pods are given their normal grace period to shut down, and then the high-priority Pod takes the freed space.
A hospital emergency ward with no free beds may move a stable patient to a corridor to free a bed for a critical arrival. Priority is the severity score. Preemption is the act of moving the stable patient.
What changes with several schedulers
Each scheduler evaluates preemption on its own cache. A custom scheduler may decide to evict Pods that were placed by the default scheduler, and the reverse is also possible. If the two schedulers are not coordinated, they can end up in a cycle where one evicts Pods that the other had just placed, and the evicted Pods get scheduled again somewhere else, possibly triggering more evictions. In a partitioned setup, where the nodes are separate, this is largely avoided because each scheduler can only evict Pods on nodes it manages.
Practical advice
Use consistent priority definitions
Priority classes are cluster objects, so every scheduler sees the same numbers. Agree on a small, well-documented set and avoid giving teams freedom to invent their own, which would lead to priority inflation.
Disable preemption where it is not wanted
Pods can declare that they do not preempt others, and the scheduler’s preemption plugin can be configured or disabled for a profile. For a batch-oriented scheduler in a shared pool, turning preemption off removes a large source of surprises.
Protect critical workloads with disruption budgets
Preemption tries to respect disruption budgets on a best-effort basis, but does not guarantee it. Do not treat budgets as a hard barrier against eviction by a scheduler.
Preemption terminates running workloads. If two uncoordinated schedulers preempt each other’s Pods, users will see unexplained restarts. Treat preemption policy as a design decision that must be made once for the whole cluster, not separately by each scheduler’s owner.
13Extenders, Custom Plugins and the Wider Ecosystem
Before writing your own scheduler, look at what the community and the framework already provide.
Scheduler extenders
An extender is an external web service that the scheduler calls at certain points, to filter nodes, to add scores or to take part in binding and preemption. This lets teams add custom logic in any language without rebuilding the scheduler. The price is speed, since each call goes over the network, and failure handling, since an unreachable extender can slow or block scheduling unless it is marked as optional. Extenders are generally seen as a legacy-friendly option, with in-process plugins preferred for new work.
Out-of-tree plugins
The scheduling framework lets you compile extra plugins into your own scheduler build, without changing the main Kubernetes code. These run in-process and are fast. The community project often called the scheduler-plugins collection offers ready-made plugins for things like group scheduling and capacity-aware scheduling, and is a good first place to look before inventing anything.
Alternative schedulers for specialised worlds
Batch and high-performance computing schedulers
Projects built for batch workloads provide ideas such as queues, fair sharing between teams, gang scheduling and job priorities. Some of them run as a separate scheduler and ask Pods to use their scheduler name. They are popular for machine learning and big data.
Resource scheduler projects for multi-tenant big data
Some projects provide hierarchical queues and resource guarantees per tenant, similar to what older big data platforms offered, and plug into Kubernetes by acting as a scheduler for the Pods that choose them.
Job queueing controllers
Not every batch tool replaces the scheduler. Some act earlier, deciding when a whole job is allowed to start, and then let the ordinary scheduler place its Pods. They work with the default scheduler, so they avoid the multi-scheduler race entirely. Knowing this difference helps when comparing tools, because a queueing controller and a replacement scheduler solve different parts of the problem.
| Tool style | Where it acts | Does it add a scheduler name? | Race risk with default scheduler |
|---|---|---|---|
| Profile in default scheduler | Inside the scheduler | Yes, a profile name | None |
| In-process plugin | Inside a scheduler build | Optional | None if it replaces the default, otherwise depends on setup |
| Extender service | Called by the scheduler | No | None |
| Replacement scheduler | Separate program | Yes | Yes unless partitioned |
| Job queueing controller | Before scheduling | No | None |
Ecosystem projects evolve quickly, and their compatibility with each Kubernetes release varies. Before adopting one, read its current documentation for supported versions, and test it in a non-production cluster first.
14Real-World Use Cases
Where teams actually reach for multi-scheduler setups, and what each one gains.
Machine learning training platforms
Distributed training jobs need every worker Pod to start together. A scheduling policy that holds back a group until all members can be placed prevents the cluster from filling with half-started jobs that block each other. Many ML platforms therefore run a gang-aware scheduler or profile for training namespaces, while serving workloads continue to use the default.
Cost optimisation for batch clusters
A batch profile that packs Pods tightly lets autoscalers remove empty nodes. The default scheduler’s spreading preference would leave all nodes lightly used, making them hard to remove. Teams often keep service workloads on the spread profile and move offline jobs onto the packing profile.
Multi-tenant platforms
A platform team serving many internal teams may offer a premium, isolated scheduling policy to a few teams, with dedicated nodes and custom rules, while the rest share the default policy. Separate scheduler names combined with node pools make the boundary clear.
Safe rollout of scheduler changes
Platform engineers who want to change scoring weights or test a new plugin do not apply it cluster-wide. They create a second profile, route a few non-critical workloads to it, compare placement quality and scheduling latency, and only then change the default.
Specialised hardware and topology
Workloads that depend on tight hardware locality, such as low-latency networking or accelerator interconnects, can benefit from a policy aware of that topology, which the default may not model fully for every device type.
Managed clusters with locked control planes
When the provider controls the built-in scheduler, running an additional scheduler as a normal workload is often the only supported way to apply custom placement behaviour for selected Pods.
15Performance and Scalability
Scheduling speed matters most when large bursts of Pods arrive together.
What makes scheduling slow
Scheduling cost grows with the number of Pods waiting, the number of nodes, and the number and cost of the plugins used. Expensive plugins, such as those that examine relationships between Pods, are costly because the work multiplies by the number of nodes and existing Pods. Extenders add network delays to each decision. Large queues of unschedulable Pods that keep being retried can also burn CPU.
Tuning options
Limit nodes scored
In big clusters the scheduler can score only a percentage of feasible nodes rather than all of them. This trades a tiny drop in placement quality for a big gain in speed.
Trim plugin lists
Disable plugins that a profile does not need. Every enabled plugin costs time on every Pod.
Keep extenders fast and optional
Use short timeouts, and mark non-essential extenders as ignorable so that their failure does not stall scheduling.
Give schedulers enough resources
The scheduler holds a cache of the whole cluster in memory and does heavy computation. Underpowered replicas are a common hidden bottleneck.
Partition to shrink the problem
When each scheduler only considers its own node pool, the number of nodes per decision falls, which can speed up decisions, although each scheduler still caches cluster state it watches.
Profiles versus separate programs for throughput
A single scheduler process makes decisions one Pod at a time, so heavy load on one profile can delay Pods of another profile sharing the queue. A separate scheduler program has its own decision loop and can work in parallel with the first, so total throughput can be higher. This is one of the few genuine performance arguments in favour of a second program. If you have a high-volume batch workload next to latency-sensitive services, isolating them in separate programs can protect service placement latency.
The very thing that makes a second program faster, the independent decision loop, is what introduces double-booking. If you add a separate scheduler for throughput, partition the nodes at the same time. Speed without partitioning trades one problem for a worse one.
| Symptom | Likely cause | First thing to check |
|---|---|---|
| Pods wait long before placement | Deep queue or slow plugins | Queue size and per-stage timings |
| Scheduler CPU stays very high | Many unschedulable Pods retried | Why Pods cannot fit and which events wake them |
| Bursts delay every workload | One shared queue and loop | Consider separating heavy batch into its own program |
| Rising count of rejected Pods on nodes | Two schedulers sharing nodes | Partitioning and node selection rules |
16Security Considerations
A scheduler decides where code runs. That makes it a sensitive component to protect and to constrain.
Permissions for each scheduler
A scheduler needs to read many object kinds and to create bindings and events. It does not need to read Secrets or to modify Deployments. Binding it to the standard scheduler roles, and nothing more, keeps its access small. A custom scheduler built with extra features may need extra permissions, which should be added deliberately and reviewed. A scheduler with broad rights is an attractive target, because anyone who controls it also controls where every Pod lands.
Who may choose a scheduler
Any user who can create Pods can write any scheduler name into them. In a shared cluster that is risky. A tenant could pick a permissive custom scheduler to escape placement rules enforced by the default one, or send Pods to nodes they should not use, or choose a non-existent name and leave Pods pending forever. Cluster rules enforced at admission time can restrict which scheduler names are allowed in which namespaces, closing this gap.
If your isolation depends on node selectors or taints that tenants can bypass by using a different scheduler name or by setting the node name directly, then your isolation is weaker than it looks. Back placement rules with admission policies and permissions, not good manners.
Trust and supply chain
Treat scheduler images like control plane software
A third-party scheduler runs with high privileges and sees the placement of every Pod. Use trusted, version-pinned images, check their sources, and scan them like any other critical infrastructure component.
Protect extender endpoints
An extender is called with Pod and node details. Secure the connection with encryption and authentication, and ensure the service cannot be reached by arbitrary workloads.
Isolate the scheduler workload itself
Run scheduler Pods in a protected namespace, restrict who can edit their configuration, and monitor changes. A silent configuration edit can change placement of the entire cluster.
Audit and detection
Audit logs record who created Pods with unusual scheduler names, who changed scheduler configurations, and who bound Pods to nodes directly. Reviewing these entries regularly helps spot misconfiguration and abuse. Alert when a Pod appears with a scheduler name that does not belong to any running scheduler, as that usually means either a typo or a probing attempt.
17Monitoring, Logging and Troubleshooting
You will need to see each scheduler’s health, and to find out quickly why a Pod is not placed.
Metrics worth watching
The scheduler exposes metrics in the standard monitoring format, and many are labelled by profile, which is very helpful when several profiles or schedulers coexist. The most useful signals are listed below in plain language.
Pending Pods
How many Pods wait in each part of the queue. Growth in the unschedulable set often signals capacity or constraint problems.
Scheduling attempts and results
Counts of attempts that ended as scheduled, unschedulable or in error, per profile. A rise in errors needs attention at once.
Scheduling latency
How long a decision takes and how long a Pod waits overall from creation to binding. Compare profiles to spot a slow one.
Per-stage durations
Time spent in each extension point, which helps pinpoint a slow plugin or extender.
Leader status
Whether each scheduler group has an active leader. A group without a leader places nothing.
Rejected Pods on nodes
Failed Pods with out-of-resource reasons are the classic fingerprint of two schedulers sharing nodes.
Reading Pod events
The easiest tool is the event list on a Pod. A successful scheduling event names the component that did it, so you can tell at once whether the default or the custom scheduler placed the Pod. A “failed scheduling” event lists, in counts, why each node was rejected, such as insufficient resources, taints or affinity. A Pod with no scheduling events at all is the warning sign for a wrong or unowned scheduler name, or a scheduler without a leader.
A troubleshooting path for a Pending Pod
Look at the Pod’s events
No events at all points to a name problem or an inactive scheduler. Events with failure reasons point to constraints or capacity.
Verify the scheduler name
Compare the name in the Pod with the names configured in running schedulers. A single character difference is enough to leave it unowned.
Check that the scheduler is running and has a leader
Look at the scheduler’s Pods, their logs and the lease record. A crashed group or colliding lock name explains silence.
Check permissions
Permission errors in the scheduler’s logs show it cannot read nodes or write bindings, often because a role binding was forgotten.
Check constraints and partitions
Pod selectors, taints and the scheduler’s own node filters may leave zero eligible nodes. Confirm that the Pod’s pool actually has capacity.
Logging with purpose
Raise scheduler log detail only temporarily, since high verbosity is very noisy. Log the Pod identity, the chosen node and the profile. Keep records of configuration changes with who made them and when. Correlate scheduler logs with node-level rejections when investigating suspected double-booking.
18Design Patterns and Best Practices
Habits that keep multi-scheduler clusters calm.
Patterns that work
Profiles first, programs last
Start with a second profile in the existing scheduler. Escalate to a separate program only when you can name the exact capability that profiles cannot provide.
One owner per node
Every node should have exactly one scheduler responsible for placing workload Pods on it. Express this through labels, taints and scheduler-level filters.
Enforce at admission, not by documentation
Automatically set or validate the scheduler name per namespace, so that workload authors cannot pick the wrong one by mistake.
Canary a scheduling change
Use a trial profile or scheduler for a small group of non-critical workloads, compare latency and placement quality, and promote the change only after a fair comparison.
Keep versions aligned
Upgrade extra schedulers with the cluster, and test them in a staging cluster during each upgrade cycle.
Document ownership and runbooks
Maintain a short list of which scheduler serves which namespaces, who owns it, and how to check its health. On-call engineers should not need to rediscover this at night.
Do and do not
Do
- Give every scheduler a unique name and a unique lease name
- Run several replicas with leader election for resilience
- Partition nodes whenever more than one program places Pods
- Bind schedulers to least-privilege roles
- Watch queue depth, latency and failure counts per profile
- Test any change in a staging cluster first
Do not
- Let two programs compete for the same nodes without protection
- Reuse the default scheduler’s lease name for another scheduler
- Leave custom schedulers on an outdated version
- Allow any tenant to set any scheduler name
- Set the node name directly in Pods as a shortcut
- Run heavy experiments in the production default profile
19Common Mistakes and Anti-Patterns
Failures that appear again and again in real clusters.
Problem
A second scheduler is deployed on the same nodes as the default, with no partitioning, to “add capacity for scheduling.”
Why It’s Harmful
Both schedulers keep separate caches and may bind different Pods to the same last free space. Nodes reject the losers, Pods fail, and replacements add load, creating a steady background of failures that look like node trouble.
Correct Approach
Give each scheduler its own node pool, enforced by labels, taints and admission rules. Or avoid a second program and use profiles in one scheduler.
Problem
The custom scheduler copies the default scheduler’s leader election settings, including the lock name.
Why It’s Harmful
The two schedulers compete for one lease. Whichever loses becomes a permanent standby, and its Pods stay Pending with no events, which is confusing to debug.
Correct Approach
Use a distinct lock name for every scheduler group, and the same name only among replicas of one scheduler.
Problem
Workloads reference a scheduler name that is mistyped, not yet deployed, or removed after a cleanup.
Why It’s Harmful
No scheduler owns those Pods, so they sit in Pending forever with no failure message, and dependent rollouts hang.
Correct Approach
Validate scheduler names with admission rules, alert on Pods that remain unscheduled with no events, and check dependencies before removing a scheduler.
Problem
A team writes a brand-new scheduler from scratch to change one scoring preference.
Why It’s Harmful
They take on years of maintenance, must track every new Pod feature, and often miss subtle behaviours such as volume binding and topology handling, leading to wrong placements.
Correct Approach
Adjust plugin weights in a profile, or add one custom plugin to the standard framework, and keep the rest of the proven scheduler logic.
Problem
Preemption is left enabled in two schedulers that share priority classes and overlapping nodes.
Why It’s Harmful
Each scheduler may evict Pods placed by the other, causing restarts, ping-pong placements and unexplained disruptions to workloads that were running fine.
Correct Approach
Decide on preemption policy once for the cluster. Disable it in schedulers where it is not needed, and keep node ownership separate.
Problem
Any user may choose any scheduler name, and isolation relies only on node selectors in the workload.
Why It’s Harmful
A tenant can bypass rules by choosing another scheduler or by writing the node name directly, landing on nodes meant for others.
Correct Approach
Enforce allowed scheduler names and node pools per namespace through admission policies, and restrict who can create Pods that name a node directly.
20Advantages, Disadvantages and Trade-offs
An honest balance sheet for the multi-scheduler approach.
Advantages
- Different workloads can get the placement behaviour they genuinely need
- Safe, gradual experimentation with new scheduling policies
- Works even when the built-in scheduler cannot be changed
- Heavy batch scheduling can be isolated from latency-sensitive services
- Teams can specialise: GPU, batch, services, premium tenants
- Can reduce cost through packing for workloads that suit it
Disadvantages / Trade-offs
- Double-booking risk when separate programs share nodes
- More components to deploy, secure, upgrade and monitor
- Partitioning lowers overall utilisation and flexibility
- Version drift can make a custom scheduler ignore newer Pod features
- Harder debugging because several decision makers exist
- Policy sprawl: each team may demand its own scheduler
Choosing between the options
| Situation | Best fit | Why |
|---|---|---|
| Different scoring preferences for two workload types | Two profiles in one scheduler | No race, minimal effort |
| Need a new filter or score rule | Custom plugin in a scheduler build | Fast, stays inside the framework |
| Need external data during placement | Extender, used carefully | Flexible, but adds network delay |
| Need gang scheduling or fair-share queues | Purpose-built batch scheduler, or a queueing controller | Provides features the default lacks |
| Strong isolation or separate release cycle for a team | Separate scheduler with partitioned nodes | Independence, at an operational cost |
| Managed control plane you cannot change | Additional scheduler as a normal workload | Only available route |
21Frequently Asked Questions
Common questions from people meeting multi-scheduler setups for the first time.
Yes. Kubernetes is designed to allow it. Each Pod names the scheduler it wants, and each scheduler only handles Pods carrying its own name. The default scheduler keeps working for every Pod that does not ask for another one.
It stays Pending indefinitely. No scheduler claims it, including the default one, and there are usually no scheduling events, which makes the problem look silent. Fixing the name or deploying the missing scheduler resolves it.
From a Pod’s point of view they look the same, because the Pod just names a scheduler. Internally a profile lives inside one scheduler program, sharing its queue and cache, while a separate scheduler is its own program with its own cache. That difference decides whether the double-booking race exists.
They can, but not without risk. If both hold stale views of free space they may promise the same capacity twice, and the node will reject the later Pod. Safe sharing is possible for light loads, yet partitioning nodes is the dependable solution.
No. Running Pods stay where they are. The change causes a rolling update, and only the newly created Pods are handled by the new scheduler.
Because only one replica should decide at a time. Two active copies of the same scheduler would behave like two competing schedulers. Standby replicas keep warm caches and take over quickly if the leader stops.
Yes, you set the scheduler name to the profile’s name in the Pod template. No other change is needed. Workloads that never set a name continue to use the default profile.
Rarely. Most needs are met by plugin weights, extra plugins or an existing project. A fully custom scheduler brings a heavy long-term maintenance burden and is justified only when nothing else fits.
Look at the Pod’s events. The successful scheduling event names the component responsible. Comparing it with the scheduler name in the Pod confirms that routing worked as intended.
Yes. A Pod with a node name already set is not scheduled by anyone, and scheduler filters are skipped, though the node’s own admission check still applies. Limit who can create such Pods if your isolation depends on scheduling rules.
Not usually. Many queueing controllers decide when a job may start and then let the ordinary scheduler place its Pods. They avoid the double-booking risk, but they do not provide custom node-selection rules. Always read a tool’s description to see whether it replaces the scheduler or sits in front of it.
Check that the scheduler name in the Pod exactly matches a running scheduler, then that the scheduler has an active leader and enough permissions. Those three checks solve most cases quickly.
22Summary and Key Takeaways
The Kubernetes scheduler picks a node for every new Pod by filtering unsuitable nodes and scoring the rest, working from a private in-memory picture of the cluster. Pods choose their scheduler with a single name field, and each scheduler only handles Pods carrying its own name. When one policy is not enough you can add profiles to the same scheduler, extend it with plugins or extenders, or run separate scheduler programs. The first two keep one decision maker and one cache, so they are the safest. The last gives full independence but introduces the central risk of this topic: two caches, two decision loops and the chance that both promise the same free space to different Pods.
Return to the airport from the beginning. Several gate controllers are fine if each owns separate gates, if each has its own radio channel, and if a supervisor makes sure that no passenger can walk through an unmarked door. In Kubernetes terms, that means partitioned node pools, unique lease names, admission rules and good monitoring. With those in place, multiple schedulers let each kind of workload get the placement it deserves, without turning your cluster into a place where Pods fight over the last seat.
Key Takeaways
- Scheduler name routes Pods — each Pod names its scheduler, and an unknown name leaves the Pod Pending with no events.
- Profiles first — several profiles in one scheduler share one queue and one cache, so they avoid double-booking and are the lowest-risk way to get different behaviour.
- Separate programs need partitioning — independent caches can both promise the same free space, and the kubelet rejects the loser, so give each scheduler its own node pool.
- Unique lease names — replicas of one scheduler share a lease, different schedulers need different leases, and a collision silently idles one of them.
- Coordinate priority and preemption — decide the policy once for the whole cluster, since uncoordinated preemption can evict and re-evict the same Pods.
- Enforce, do not hope — use admission policies and least-privilege roles to control who may choose which scheduler and which nodes, and keep versions aligned with the cluster.
- Monitor each scheduler separately — watch queues, attempts, latency, leader status and node-level rejections, and use Pod events to see which scheduler acted.