Multi-Scheduler Setups in Kubernetes

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.

Simple Analogy

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.

Need 1

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.

Need 2

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.

Need 3

Hardware awareness

GPUs, special network cards or topology rules may need custom scoring that the default does not know about.

Need 4

Experiments and safety

Platform teams want to test a new policy on a small set of workloads before trusting it with the whole cluster.

Need 5

Tenant isolation

Different teams or business units may need different placement rules without touching each other.

!
Common Misconception

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.

Simple Analogy

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.

Queue part

Active queue

Pods that are ready to be tried right now, ordered by priority and arrival time.

Queue part

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.

Queue part

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.

i
Remember This

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.

Simple Analogy

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.

1

User creates a workload

The Pod template carries a scheduler name, either explicit or the default one filled in by the API server.

2

The Pod appears unassigned

It is stored with an empty node field, and every scheduler’s informer sees it appear.

3

Each scheduler checks the name

Only the scheduler whose name matches adds the Pod to its queue. All others skip it immediately.

4

The owner schedules and binds

The matching scheduler picks a node, writes the binding, and the kubelet takes over.

i
Bypassing Schedulers Entirely

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.

AspectProfiles in one schedulerSeparate scheduler programsExtenders and plugins
Process countOneTwo or moreOne, plus optional web service
Shared view of clusterYes, one cache and one queueNo, each has its own cacheYes, inside the same scheduler
Risk of two schedulers racing for a nodeVery low, decisions are serialisedReal and must be managedNone, still one decision maker
Operational effortLow, one configuration fileHigh, separate deployment, access rules and monitoringMedium, build or host extra pieces
Freedom to use a totally different algorithmLimited to available pluginsCompleteModerate
Good first choice?Yes for most casesOnly when truly necessaryWhen you need custom logic inside the framework
i
A Practical Rule of Thumb

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"]
        
FIG 1 — A decision path for choosing between profiles, plugins and separate schedulers

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.

Stage 1

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.

Stage 2

Pre-filter and filter

Prepares information and then removes nodes that cannot host the Pod.

Stage 3

Post-filter

Runs only when no node fits. This is where preemption lives: deciding whether lower-priority Pods could be evicted to make room.

Stage 4

Score and normalise

Each plugin rates the surviving nodes, ratings are brought to a common scale, and weighted totals pick the winner.

Stage 5

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.

Stage 6

Pre-bind, bind and post-bind

Final preparations such as attaching volumes, then the actual binding to the node, then any cleanup.

Simple Analogy

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"]
        
FIG 2 — The scheduling framework: each box is an extension point where plugins plug in

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.

!
One Queue-Sort Rule

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.

Simple Analogy

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
i
Profile Settings Worth Knowing

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.

1

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.

2

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.

3

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.

4

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.

5

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.

6

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.

!
Frequent Pitfall

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.

Simple Analogy

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
        
FIG 3 — Two scheduler groups, each with its own lease, each with exactly one active leader

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.

i
Diagnosing a Silent Scheduler

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
        
FIG 4 — Two schedulers share one API server, each handling only the Pods that carry its name

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.

2
independent caches, one per scheduler program
1
shared source of truth: the API server
ms
typical gap before one scheduler sees the other’s decision

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.

Simple Analogy

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
        
FIG 5 — Both schedulers bind to the last free space, and the kubelet rejects the loser

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.

!
Special Case: Non-Resource Conflicts

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.

“Two schedulers do not break each other. They break the node they both believe is empty.”

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.

Tool 1

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.

Tool 2

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.

Tool 3

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.

Tool 4

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
        
FIG 6 — Each scheduler owns a distinct node pool, so double-booking becomes impossible

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
i
DaemonSet and System Pods

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.

Simple Analogy

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.

!
Evictions Are Real Disruptions

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 styleWhere it actsDoes it add a scheduler name?Race risk with default scheduler
Profile in default schedulerInside the schedulerYes, a profile nameNone
In-process pluginInside a scheduler buildOptionalNone if it replaces the default, otherwise depends on setup
Extender serviceCalled by the schedulerNoNone
Replacement schedulerSeparate programYesYes unless partitioned
Job queueing controllerBefore schedulingNoNone
i
Check Versions and Maturity

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.

Gang
all-or-nothing starts for training jobs
Pack
tight placement so empty nodes can be removed
Trial
safe experiments on a small set of workloads

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

Lever 1

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.

Lever 2

Trim plugin lists

Disable plugins that a profile does not need. Every enabled plugin costs time on every Pod.

Lever 3

Keep extenders fast and optional

Use short timeouts, and mark non-essential extenders as ignorable so that their failure does not stall scheduling.

Lever 4

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.

Lever 5

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.

!
Parallelism Brings Back the Race

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.

SymptomLikely causeFirst thing to check
Pods wait long before placementDeep queue or slow pluginsQueue size and per-stage timings
Scheduler CPU stays very highMany unschedulable Pods retriedWhy Pods cannot fit and which events wake them
Bursts delay every workloadOne shared queue and loopConsider separating heavy batch into its own program
Rising count of rejected Pods on nodesTwo schedulers sharing nodesPartitioning 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.

!
Placement Rules Are Only as Strong as Their Enforcement

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.

Metric idea

Pending Pods

How many Pods wait in each part of the queue. Growth in the unschedulable set often signals capacity or constraint problems.

Metric idea

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.

Metric idea

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.

Metric idea

Per-stage durations

Time spent in each extension point, which helps pinpoint a slow plugin or extender.

Metric idea

Leader status

Whether each scheduler group has an active leader. A group without a leader places nothing.

Cluster signal

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

1

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.

2

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.

3

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.

4

Check permissions

Permission errors in the scheduler’s logs show it cannot read nodes or write bindings, often because a role binding was forgotten.

5

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.

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

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

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

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

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

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

SituationBest fitWhy
Different scoring preferences for two workload typesTwo profiles in one schedulerNo race, minimal effort
Need a new filter or score ruleCustom plugin in a scheduler buildFast, stays inside the framework
Need external data during placementExtender, used carefullyFlexible, but adds network delay
Need gang scheduling or fair-share queuesPurpose-built batch scheduler, or a queueing controllerProvides features the default lacks
Strong isolation or separate release cycle for a teamSeparate scheduler with partitioned nodesIndependence, at an operational cost
Managed control plane you cannot changeAdditional scheduler as a normal workloadOnly available route

21Frequently Asked Questions

Common questions from people meeting multi-scheduler setups for the first time.

Q1Can I run more than one scheduler in a cluster at all?

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.

Q2What happens to a Pod that names a scheduler that does not exist?

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.

Q3Are profiles the same as multiple schedulers?

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.

Q4Can two schedulers really place Pods on the same node safely?

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.

Q5Does changing the scheduler name on a Deployment move running Pods?

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.

Q6Why do replicas of one scheduler need leader election?

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.

Q7Do I need to change the Pod to use a different profile?

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.

Q8Should I write my own scheduler?

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.

Q9How do I know which scheduler placed a given Pod?

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.

Q10Can a user bypass a scheduler by setting the node name?

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.

Q11Is a job queueing tool a second scheduler?

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.

Q12What is the first thing to check when custom-scheduler Pods stay Pending?

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.