ASAysha Shafique
← All project blogs

Comparing Linux's EEVDF and CFS schedulers under contention

I put Linux's EEVDF and CFS schedulers under controlled contention and found that prompt scheduling is not the same thing as useful progress.

014–6× lower p99.9 latency with EEVDF
02Up to 65% higher Redis GET throughput
0310–15% more 7-Zip throughput with CFS

The question

A general-purpose CPU scheduler has to share one finite resource among tasks that want very different things. An interactive process wants to wake up the instant it has work. A compression job wants long, uninterrupted runs. A server needs both prompt dispatch and enough contiguous CPU time to drain its queue.

For years, Linux reconciled these with the Completely Fair Scheduler (CFS). CFS tracks each task’s virtual runtime to hand out CPU time proportionally, but it has no native way to tell a latency-sensitive task apart from one that just wants more total CPU. To compensate, the kernel accreted a pile of fragile latency heuristics. In 2023, kernel 6.6 replaced CFS with EEVDF: Earliest Eligible Virtual Deadline First. EEVDF tracks each task’s lag, the gap between the CPU time it was owed and the time it actually got, so positive lag means a task is behind. It combines an eligibility rule with a virtual deadline, so a task that is owed time can be dispatched promptly without breaking long-term proportional sharing.

The idea is old. Stoica and Abdel-Wahab described EEVDF in 1995, part of the same virtual-time scheduling lineage as later designs like Borrowed Virtual Time. But its arrival as Linux’s default scheduler is recent, and the theory’s latency bounds are only interesting if they survive contact with real applications. That was my question: does waking sooner actually produce smoother video, lower server latency, and more throughput? The short answer is that a task can wake quickly and still make poor progress if its execution is repeatedly fragmented.

The test bed

I ran controlled experiments on equivalent bare-metal CloudLab nodes with 40 logical x86_64 CPUs. The EEVDF system ran Linux 6.17.10; the CFS baseline ran 6.1.158. Both used Ubuntu 24.04. CPU-frequency scaling was disabled to fix the clock, background workers saturated the cores to sustain run-queue contention, and every experiment was repeated to average out system noise.

I measured the scheduler from two directions. Application metrics told me what a user or operator would feel: MPV video frame drops, Python HTTP request latency under wrk, Redis GET and SET throughput, schbench wakeup latency, and 7-Zip compression throughput. Each was paired with a competing background load, stress-ng or 7-Zip, across five scenarios. I swept CPU load from a handful of workers to extreme oversubscription at 512 threads, and ran every pairing at both equal and differentiated priority.

Kernel tracing told me why. Using eBPF, a kretprobe on pick_next_task_fair recorded each task’s virtual lag at dispatch, and sched_wakeup and sched_switch events gave me preemption frequency. That let me connect a high-level result, a dropped frame or a slow request, back to the exact preemption and fragmentation behavior that caused it.

  • Equal priority: foreground and background tasks both at nice 0.
  • Differentiated priority: foreground at nice -15 and background at nice +15.
  • Load swept from a handful of workers to extreme oversubscription at 512 threads.
  • eBPF tied virtual lag and context-switch counts to application behavior.

Tail latency: EEVDF delivered on its central promise

The clearest EEVDF win was at the far end of the wakeup-latency distribution. With schbench competing against a CPU-bound load, EEVDF held 99.9th-percentile wakeup latency far below CFS as contention rose.

Under equal priority, EEVDF’s p99.9 latency stayed roughly 9–35 milliseconds all the way to 512 threads, while CFS blew past 100 milliseconds at high thread counts. With differentiated priorities, EEVDF held below 7 milliseconds at 512 threads, four to six times lower than CFS.

That gap is what matters for interactive and real-time-adjacent systems. The median can look perfectly healthy while the slowest 0.1% of wakeups cause missed deadlines, timeouts, or visible stalls. EEVDF’s bounded-lag model constrains exactly that worst case.

Tail latency under contention CFS p99.9 latency relative to EEVDF
Equal priorityCFS p99.9 wakeup latency vs EEVDF · 512 threads
Differentiated priorityCFS p99.9 wakeup latency vs EEVDF · 512 threads
Representative tail-latency points. Each figure is how much longer CFS took to wake the benchmark task at the 99.9th percentile, relative to EEVDF at 512 threads. The advantage becomes decisive as priorities are differentiated under heavy contention.

Real workloads: the results stopped agreeing

Redis favored EEVDF when every task had equal priority. At 40 stress workers, GET throughput was about 40,000 requests per second under EEVDF versus 24,000 under CFS, a 65% advantage. SET showed the same shape, 57% higher at high contention.

Explicit priorities erased most of that. With Redis at nice -15 and stress-ng at nice +15, both schedulers converged to roughly the same throughput. Configuration compensated for the scheduling difference, which suggests EEVDF’s real edge shows up in unconfigured or shifting environments, where it has to infer urgency from behavior rather than being told.

The HTTP server went the other way. Under differentiated priorities, CFS held 13–59% lower average latency. EEVDF woke the server threads promptly, but its short default slices and bounded-fairness corrections preempted them constantly. CFS let low-priority tasks fall far behind in virtual runtime, so the server ran in longer uninterrupted bursts that preserved cache state, relieved socket-queue back-pressure, and drained more requests per wakeup.

Where each scheduler won Largest observed advantage
EEVDF advantage CFS advantage
Horizontal bar chart summarizing the largest observed EEVDF and CFS workload advantages010%10%20%20%30%30%40%40%50%50%60%60%70%70%← CFS betterEEVDF better →Redis GET: 65% (EEVDF)Redis GET65%Redis SET: 57% (EEVDF)Redis SET57%HTTP latency: 59% (CFS)HTTP latency59%7-Zip throughput: 15% (CFS)7-Zip throughput15%Largest observed advantage (%)
Representative workload outcomes. EEVDF led the equal-priority Redis runs; CFS led HTTP latency with differentiated priorities and compression throughput under heavy schbench contention.
  • Redis GET: up to 65% more throughput with EEVDF at equal priority.
  • Redis SET: up to 57% more throughput with EEVDF at equal priority.
  • HTTP: 13–59% lower average latency with CFS under differentiated priorities.
  • Priority tuning largely erased the Redis gap.

Fragmentation: video made execution contiguity visible

At equal priority, CFS and EEVDF dropped almost the same number of MPV frames as load climbed: 557 versus 560 of 603 frames at 160 stress workers. Once priorities were differentiated, CFS actually did better under heavy stress, 453 versus 464 dropped frames at 160 workers, even though EEVDF is the scheduler designed to help interactive work.

Video playback under heavy contention Dropped frames · 160 stress workers
EEVDF CFS
Horizontal bar chart comparing dropped MPV video frames under equal and differentiated priorities0100200300400500600603 total framesEqual priorityEqual priority · EEVDF: 560EEVDF560Equal priority · CFS: 557CFS557DifferentiatedDifferentiated · EEVDF: 464EEVDF464Differentiated · CFS: 453CFS453Dropped frames · 160 stress workers · lower is better
Video playback at the heaviest tested load. The schedulers are nearly tied at equal priority. Favoring MPV reduces drops for both, but CFS retains a small advantage.

The eBPF traces resolved the apparent contradiction. EEVDF gave MPV prompt but fragmented execution, preempting it every 2.8 milliseconds or so to keep every runnable task, including the low-priority stress workers, within its lag bound. CFS let the high-priority player run in longer contiguous intervals. For 60 fps playback, each frame has about a 16.7 ms budget to decode and present, so waking quickly is not enough if the run is chopped into slices too short to finish a frame.

I then varied MPV’s requested slice directly with sched_setattr, and the relationship was non-monotonic. Very small slices (≤ 0.4 ms) fragmented execution into 10,500–11,600 context switches per run and dropped many frames. Intermediate slices (0.6–0.8 ms) minimized drops. But pushing the slice up to about 3.2 ms made MPV accrue positive lag, so EEVDF scheduled the background tasks more aggressively to compensate and frames started dropping again. The useful quantity was not more CPU time. It was appropriately sized uninterrupted time.

The trade-off: better tail guarantees have a throughput price

The schbench and 7-Zip pairing showed the cost head-on. EEVDF protected wakeup latency while CFS sustained more compression throughput. At 256–512 schbench threads, CFS completed roughly 10–15% more 7-Zip work.

Frequent preemption shortens effective run lengths, disrupts cache locality, and limits instruction-level efficiency. CFS lets CPU-bound work run in longer bursts, which weakens its worst-case latency guarantees but rewards throughput-heavy computation. That is the whole trade-off in a single experiment.

There is no universal winner. EEVDF marks a shift from fragile heuristics to a scheduler with real mathematical fairness guarantees, and it is the right choice when tail responsiveness has to stay bounded through unexpected contention. CFS still wins when cache warmth, execution continuity, and raw throughput matter more. This is a small study on a handful of workloads across two kernels, so I would want more before generalizing. But the pattern was consistent: for state-heavy services, the answer is not to abandon bounded fairness. It is to give applications a way to express the execution granularity they need, through interfaces like sched_setattr, so that promptness and contiguity stop fighting each other.

  • Choose EEVDF for predictable tail behavior under oversubscription.
  • Expect a throughput cost when frequent preemption fragments state-heavy work.
  • Use explicit priority configuration when workload importance is known.
  • Treat time-slice length as a workload property, not a universal constant.
NEXT PROJECT LOGPrivilege separation for a Gradescope autograder
Where to?