You can write a CPU scheduler in BPF now. Most of you shouldn't. Some of you really should.
Published by RodHat

The sched_ext framework merged into Linux 6.12 in November 2024. One kernel
release cycle and a handful of distro updates later, it is in the kernel your
servers are already running if you are on anything current. Which means you can
now write a CPU scheduler as a BPF program, load it onto a live system, and have
the kernel execute it. No reboot. No out-of-tree patches. No rebuilding from
source every time upstream moves.
The kernel verifier checks your BPF for correctness before it runs. If your scheduler lets a runqueue stall long enough to trigger the watchdog, the system falls back to the Completely Fair Scheduler automatically and keeps running.
I spent a weekend with it. My read changed. This is not irresponsible. This is the right design.
Why the existing options were bad
The Completely Fair Scheduler has been the Linux default since 2.6.23 in 2007. It handles general-purpose workloads competently. It is correctly inflexible when your workload is not general-purpose.
If you are running a latency-sensitive service and CFS scheduling decisions are eating your tail latency, you had four options before 6.12:
Tune the scheduler tunables and hope. There are sched_latency_ns,
sched_min_granularity_ns, and assorted other knobs. They move the problem
around rather than solving it.
Use a real-time priority class. SCHED_FIFO or SCHED_RR gets you priority
but adds its own failure modes: a misbehaving RT process starves everything else.
You are trading one scheduling problem for a different, more dangerous one.
Patch a custom scheduler into your kernel. Now you maintain kernel patches, you rebuild on every upstream update, and your ops team hates you. Congratulations, you now own that.
Run the PREEMPT_RT kernel. Legitimate for actual real-time requirements. Requires out-of-tree patches and its own build and maintenance burden.
sched_ext adds a fifth option: write the policy yourself, in BPF, load it at
runtime, and let the kernel’s safety infrastructure catch the ways you will
inevitably be wrong.
The safety net is the whole argument
BPF programs pass through the kernel verifier before execution. The verifier statically proves that your program terminates, accesses only valid memory, and does not contain unbounded loops. This is not optional and it is not advisory. Your BPF scheduler either passes the verifier or it does not load. Full stop.
The watchdog layer sits on top of that. If your scheduler lets a runqueue get stuck for too long, the built-in watchdog fires and the system reverts to CFS. You do not kernel panic. You do not need console access. The machine keeps running while you go look at what your BPF program did wrong.
This is the specific design decision that changed my read. Writing a custom scheduler you trust on production hardware without a net is a massive ask. Writing one where the verifier checks correctness first and the watchdog catches the cases the verifier cannot statically reason about is a tractable engineering problem. The failure modes were anticipated and handled before the feature shipped. That is not nothing.
The scx schedulers are production code
The scx repository collects BPF schedulers written by people who know what they
are doing. A few worth understanding:
scx_lavd is a latency-aware virtual deadline scheduler built for interactive
and gaming workloads. It tracks per-task “performance criticality” derived from
actual observed scheduling behavior, then prefers to run high-criticality tasks
on the most capable CPUs. On asymmetric core systems (Intel P-cores and E-cores,
AMD’s mixed topology), this matters. CFS does not have a model of per-core
single-thread performance. Its placement decisions reflect that gap. scx_lavd
addresses it.
scx_rusty demonstrates the two-component architecture that more complex
schedulers use: a BPF program handles the fast path (which CPU runs this task,
when to preempt), and a userspace process written in Rust handles policy decisions
that tolerate latency (inter-NUMA load balancing, weight tuning). The userspace
process runs as a normal process. If it crashes, the watchdog handles the
transition back to CFS. The separation keeps the fast path as lean as possible
while allowing arbitrarily complex policy logic off the hot path.
scx_simple is the starting point for reading: the minimum viable scheduler
implementation with no production ambitions and clear structure. Read this before
reading anything else if you want to understand the hook model.
What this is actually for
You are not going to write a better general-purpose scheduler than CFS in a weekend. CFS has seventeen years of Linux kernel developer time in it, has been beaten on by workload analysis from every major cloud vendor, and its pathological cases are well-documented and partially addressed. Write a naive FIFO scheduler in BPF and it will perform worse than CFS in almost every benchmark you run.
What you might actually do:
Write a scheduler for a database machine with asymmetric cores that pins query threads to P-cores and background compaction to E-cores, based on cgroup membership, instead of hoping CFS figures it out. Write a game server scheduler that prioritizes the network input thread over physics and asset streaming threads using a simple policy that CFS’s fairness model cannot express. Prototype a custom scheduling policy for a specific workload, measure it against CFS on your actual hardware with your actual traffic, and iterate on it without rebuilding the kernel between experiments.
The last one is the real unlock. The edit-load-measure cycle for a BPF scheduler is seconds. The same cycle for a kernel patch is measured in minutes at best, longer in practice when you add the reboot. That iteration speed compounds into significantly better outcomes for anyone who cares enough to use it.
The feature is not a toy. It is also not for anyone who does not know what a runqueue is or why scheduler latency shows up in application tail latency. Those two things are both true simultaneously.
One more thing, and I mean this: the watchdog fallback and the BPF verifier
together make sched_ext safer than I expected when I first read the patchset
description. The design clearly modeled its own failure modes and built
containment for each one before merging. More kernel features should work this
way.
See also: cgroups v2 without systemd for the resource isolation layer your custom scheduler will want to integrate with, and bpftrace one-liners for IO latency profiling for the observability tooling that makes sense of what your scheduler is actually doing to your workload.
Sources
- sched_ext: BPF-extensible scheduler class — Linux kernel documentation (The Linux Kernel project)
- sched-ext/scx: sched_ext schedulers and tools (sched-ext project (GitHub))