Shortest-Job-First Scheduling
Imagine a print server with ten jobs queued. There are nine one-page memos and one three-hundred-page thesis. Under First-Come First-Served Scheduling, if the thesis arrives first, every short memo waits behind it. Average waiting time balloons as a result. Shortest-Job-First (SJF) scheduling fixes this. It always runs the job with the smallest remaining work next. This lets the many small jobs flow through quickly. The one long job merely delays itself. This idea matters because average waiting time is a key measure of scheduler quality. SJF is not merely a good heuristic. It is provably optimal for minimizing average waiting time. This holds among non-preemptive schedulers, given known burst times. Its preemptive cousin, Shortest-Remaining-Time-First (SRTF), extends this optimality further. It covers cases where jobs arrive at different times. Such jobs can also be interrupted mid-execution. Real operating systems rarely run pure SJF. This is because it requires knowing the future. But its logic underlies burst-time estimation techniques. It also motivates the priority and multilevel-feedback schedulers used in practice. The big picture: SJF represents the theoretical ceiling for many scheduling metrics. This is much as Dijkstra's algorithm represents the ceiling for shortest-path search. That search applies to graphs with non-negative weights. Every practical scheduler is implicitly judged against SJF's average waiting time. However, SJF requires an oracle for future CPU burst lengths. No real system possesses such an oracle. This article develops the algorithm precisely. It proves optimality with an exchange argument. It shows how real systems approximate unknowable future burst time. This is done through exponential averaging. It also confronts the algorithm's fatal practical flaw: starvation of long jobs. We also build the intuition needed for Priority Scheduling and Round-Robin Scheduling. Both of these can be read as responses to SJF's weaknesses.
- SJF picks the process with the smallest CPU burst next; SRTF is its preemptive variant, re-evaluating on every arrival.
- SJF (non-preemptive, all jobs known) provably minimizes average waiting time — an exchange-argument proof.
- SRTF minimizes average waiting time even when jobs arrive at different times, by always preferring the shortest remaining work.
- Real burst times are unknown in advance; systems predict them via exponential averaging of past bursts.
- SJF/SRTF can starve long jobs indefinitely if short jobs keep arriving — the central practical weakness.
- These ideas motivate Priority Scheduling (generalizes "shortest first") and Round-Robin Scheduling (fixes starvation via time slicing).
Running the shortest job first is optimal for average waiting time because it lets the largest possible number of processes finish before any single long process delays them.
The idea
Shortest-Job-First (SJF) scheduling selects, from the set of processes ready to run, the one with the smallest CPU burst time — the length of its next uninterrupted stretch of CPU use. In the non-preemptive form, once a process starts running it continues until it either finishes or voluntarily blocks (e.g., for I/O); the scheduler only chooses among waiting jobs at those decision points. In the preemptive form, called Shortest-Remaining-Time-First (SRTF), the scheduler re-evaluates every time a new process arrives: if the newcomer's total burst is shorter than the remaining burst of the currently running process, the running process is preempted immediately. The key performance metric SJF targets is average waiting time, , where is the time process spends ready but not running (see Scheduling Performance Metrics). SJF's central theoretical result is that among all non-preemptive scheduling disciplines applied to a fixed, known set of burst times all available at time zero, ordering by ascending burst time minimizes . SRTF extends this to dynamic arrivals: at every instant, running whichever ready process has the least remaining work minimizes cumulative waiting time, again assuming perfect knowledge of future bursts. The catch, which the rest of this article treats seriously rather than glossing over, is that no scheduler can observe a process's future CPU burst directly — it must be predicted from the process's own history, typically via an exponentially weighted moving average. And because short jobs are always preferred, a steady stream of newly arriving short jobs can delay a long job forever: this is starvation, the price SJF pays for its optimality.
How it works
Non-preemptive SJF. At every scheduling decision point (CPU becomes idle, or the running process finishes/blocks), scan the ready queue and dispatch the process with the smallest burst time. Ties are broken by arrival order (FCFS) or process ID, by convention.
SRTF. Maintain, for the running process, its remaining burst time, decremented as it executes. On every arrival event, compare the new process's total burst against the running process's remaining burst; if the new one is strictly smaller, preempt. On every completion event, pick the minimum-remaining-burst process from the ready queue. The efficient implementation uses a min-heap (priority queue) keyed on (remaining) burst time, so that "find the smallest" is rather than a linear scan. ``` // SRTF scheduler using an event-driven simulation with a min-heap // ready_heap: min-heap of (remaining_burst, arrival_time, pid) // events: arrivals sorted by arrival_time function SRTF_schedule(processes): sort processes by arrival_time ready_heap = empty min-heap current = NONE // currently running process time = 0 completed = [] i = 0 // index into arrival-sorted processes n = len(processes) while len(completed) < n: // 1. Admit all processes that have arrived by 'time' while i < n and processes[i].arrival_time <= time: push(ready_heap, processes[i]) i += 1 // 2. If nothing to run, jump to next arrival (idle CPU) if current == NONE and ready_heap is empty: time = processes[i].arrival_time continue // 3. Choose the process with smallest remaining burst candidate = peek_min(ready_heap) if current == NONE or candidate.remaining < current.remaining: if current != NONE: push(ready_heap, current) // preempt: requeue current = pop_min(ready_heap) // may equal candidate // else current keeps running // 4. Advance time by 1 unit (or until next event, for efficiency) next_event_time = min(next_arrival_time(i, processes), time + current.remaining) run_for = next_event_time - time current.remaining -= run_for time = next_event_time // 5. Handle completion if current.remaining == 0: current.completion_time = time completed.append(current) current = NONE return completed ```
Correctness of the event-driven step: instead of simulating one time unit at a time — which is correct but slow — the loop advances directly to the next meaningful event: either the next arrival or the current process's completion, whichever comes first. No preemption decision can occur strictly between these two events, because the ready-process set and the running process's remaining time only change at those moments. This turns an simulation (where is total time) into an -event simulation.
Complexity & correctness
Time complexity. With a min-heap of size at most , each arrival is one insertion and each preemption/dispatch decision is one extract-min, giving total for processes across the whole schedule — because there are at most arrival and completion events. Non-preemptive SJF over a static batch is simply sorting: , dominated by the sort itself (or if bursts are already sorted or bucketed).
Space complexity is for the ready-queue heap and per-process bookkeeping.
Correctness (optimality of non-preemptive SJF, static case). Claim: if all processes are available at time 0 with known burst times , then running them in non-decreasing order of minimizes . Proof (exchange argument). Fix any schedule that is not sorted by burst time. Then there exist two adjacent jobs (scheduled before ) with . Swapping their order changes only their own waiting times (every other job's waiting time is unaffected, since the total time consumed before and after this pair is unchanged). Before the swap, waits and waits (where is the time already elapsed before this pair starts). After swapping, waits and waits . The change in total waiting time is:
So the swap strictly decreases total waiting time whenever . Any non-sorted schedule therefore admits an improving swap, so it cannot be optimal; repeating swaps (a bubble-sort argument) drives any schedule toward the sorted order, and since total waiting time strictly decreases with each improving swap and is bounded below, the process terminates at the sorted schedule. Hence sorted-by-burst is optimal, and it is unique up to ties.
Correctness (optimality of SRTF, dynamic arrivals). The same exchange idea extends via an interval-exchange argument: at any instant where a shorter job is ready but a longer one is running, deferring the short job's start only postpones its own completion without shortening anyone else's wait, while running it first cannot lengthen any other job's wait by more than it shortens the short job's own — a formal proof uses an inductive exchange over completion order, showing any schedule can be transformed into the SRTF schedule without increasing total waiting time. The result: SRTF minimizes among all schedules (preemptive or not) given perfect knowledge of burst times, at every arrival pattern.
The honest tradeoff. This optimality is conditional on knowing the future. Real schedulers only have an estimate of the next burst, not the true value . If estimates are poor, SJF-by-estimate can perform arbitrarily worse than true SJF, and worse than simpler policies in pathological cases. Moreover, SJF/SRTF ignores fairness entirely — it optimizes a system-wide average at the expense of individual long jobs, a tradeoff made explicit below.
Worked example
Consider four processes, non-preemptive SJF is inapplicable here because arrival times differ, so we trace SRTF:
| Process | Arrival | Burst |
|---|---|---|
| P1 | 0 | 8 |
| P2 | 1 | 4 |
| P3 | 2 | 9 |
| P4 | 3 | 5 |
Trace:
| Time | Event | Ready set (remaining) | Running | Remaining after |
|---|---|---|---|---|
| 0 | P1 arrives | {P1:8} | P1 dispatched | P1:8 |
| 1 | P2 arrives | {P2:4} | compare: P2(4) < P1(7) → preempt | P1:7 (queued), P2 runs |
| 2 | P3 arrives | {P1:7, P3:9} | P3(9) > P2(3) → P2 continues | P2:3 |
| 3 | P4 arrives | {P1:7, P3:9, P4:5} | P4(5) > P2(2) → P2 continues | P2:2 |
| 5 | P2 completes | {P1:7, P3:9, P4:5} | min = P4(5) → dispatch P4 | — |
| 10 | P4 completes | {P1:7, P3:9} | min = P1(7) → dispatch P1 | — |
| 17 | P1 completes | {P3:9} | dispatch P3 | — |
| 26 | P3 completes | {} | done | — |
Completion times: P2 = 5, P4 = 10, P1 = 17, P3 = 26. Turnaround time ; waiting time .
| Process | Completion | Turnaround | Waiting |
|---|---|---|---|
| P1 | 17 | 17 − 0 = 17 | 17 − 8 = 9 |
| P2 | 5 | 5 − 1 = 4 | 4 − 4 = 0 |
| P3 | 26 | 26 − 2 = 24 | 24 − 9 = 15 |
| P4 | 10 | 10 − 3 = 7 | 7 − 5 = 2 |
Compare to FCFS on the same input (order P1, P2, P3, P4, run to completion in arrival order): P1 waits 0, P2 waits 7 (8−1), P3 waits 10 (12−2), P4 waits 16 (17−3), giving . SRTF's 6.5 is strictly lower, illustrating (not proving, since this is one instance) the general optimality result. Notice also that P3 — the longest job — accumulates the largest wait (15), a preview of the starvation problem discussed next.
Burst-time prediction. Since true bursts are unknown, systems estimate the next CPU burst from the actual duration of the most recent burst and the previous estimate , using exponential averaging:
Expanding recursively,
so each historical burst contributes with geometrically decaying weight — recent bursts matter more, and the influence of the ancient past (and the arbitrary initial guess ) vanishes as . Setting weighs recent and historical bursts equally per step; makes the estimator track only the last observed burst (fast-adapting, noisy); makes it ignore recent history (stable, slow to adapt). This estimator, not the true burst time, is what a real "SJF" scheduler actually sorts by — making it heuristic, not literally optimal.
Common pitfalls & traps
| Pitfall | Why it happens | Fix |
|---|---|---|
| Confusing SJF (non-preemptive) with SRTF (preemptive) | Textbook problems often blur which variant is meant | Always check: does a new arrival with a smaller burst interrupt the running process? |
| Comparing total burst instead of remaining burst in SRTF | Easy to forget the running process has already used some of its burst | Preemption decisions must compare the newcomer's burst against the remaining time of the current process |
| Ignoring ties | Multiple processes with equal burst | Break ties consistently (e.g., FCFS order) or results become non-deterministic across textbooks/graders |
| Treating estimated bursts as exact | Estimation error is silently dropped in classroom exercises | State explicitly that real schedulers use estimates, not true values, and errors compound |
| Forgetting idle time when the ready queue is empty | A gap between completions and next arrival | Advance the clock to the next arrival rather than assuming continuous readiness |
| Assuming SJF is "the best scheduler" unconditionally | Optimality is about average waiting time only, under a specific (often static, always-known-future) model | Note explicitly that fairness, response time for interactive use, and starvation are not covered by this optimality guarantee |
Pioneers & history
Origins · 1 minEdsger W. Dijkstra and how this was discovered
Where this matters
Pure SJF/SRTF is almost never deployed as-is in general-purpose operating systems, precisely because burst times are unknown and starvation is unacceptable for interactive workloads. But its optimality result is the theoretical anchor for an entire family of practical schedulers. Batch-processing systems (e.g., HPC job schedulers like Slurm) approximate SJF using user-supplied or historically estimated job-length hints to minimize mean queue delay, often with anti-starvation caps. Multilevel feedback queue schedulers, the ancestor of both Windows and traditional Unix schedulers, use observed CPU-burst behavior to demote CPU-bound processes and promote I/O-bound ones — effectively estimating "shortness" from recent behavior, exactly as the exponential-averaging predictor does. Network packet scheduling and disk I/O schedulers borrow the shortest-job intuition in disciplines like Shortest-Seek-Time-First. Understanding SJF is also the necessary stepping stone to Priority Scheduling, which generalizes "run the smallest burst" to "run the highest priority" (of which burst-time-based priority is one instance), and to Round-Robin Scheduling, which explicitly sacrifices SJF's optimal average waiting time to guarantee bounded response time and eliminate starvation.
Ask the tutor: How would the SRTF optimality proof change if context-switch overhead were charged for every preemption?
Is SJF ever actually implemented exactly as described, with true burst times known in advance?
Rarely, and mostly in constrained settings. Batch systems where jobs declare expected runtime (common in HPC schedulers) can approximate true SJF if users provide accurate estimates, sometimes enforced by killing jobs that exceed their declared time. In general-purpose operating systems, true future burst time is fundamentally unknowable — a process might loop forever or finish in a microsecond, and the OS cannot know which without running it. Practical systems therefore always use SJF as a predictive heuristic layered onto other mechanisms (priority boosts, aging, feedback queues), never as the literal, provably-optimal static algorithm from the theoretical model.
Does SRTF ever perform worse than FCFS in practice?
Yes, when burst-time predictions are systematically wrong. If a scheduler misestimates a genuinely short job as long (or vice versa), SRTF-by-estimate can produce a worse ordering than simple FCFS, since FCFS at least guarantees no process is perpetually deprioritized. Additionally, SRTF's frequent preemptions incur real context-switch overhead ignored in the idealized average-waiting-time model; if burst times are comparable to the context-switch cost, the theoretical gains can be swamped by practical switching overhead, making FCFS or a coarser-grained policy preferable.
How exactly does the exchange-argument proof establish optimality, and does it generalize to SRTF?
The static-case proof shows any non-sorted schedule has an adjacent inversion (a longer job before a shorter one) whose swap strictly reduces total waiting time, without affecting any other job's wait. Repeating swaps monotonically decreases total waiting time until the schedule is sorted, so sorted-by-burst must be a global minimum. For SRTF with dynamic arrivals, the argument generalizes to an interval-exchange over the timeline: at any moment a shorter-remaining job could run instead of a longer one, swapping their execution intervals cannot increase — and generally decreases — total waiting time, extending optimality to the preemptive, dynamic-arrival setting.
What's the practical difference between "burst time" and "CPU-bound vs I/O-bound" process classification?
CPU burst time is a single number: how long a process runs before it next blocks or finishes. Whether a process is "CPU-bound" or "I/O-bound" is a broader behavioral pattern across many bursts — CPU-bound processes tend to have long individual bursts, I/O-bound processes tend to have short, frequent bursts punctuated by I/O waits. SJF/SRTF scheduling decisions are made per-burst using the (predicted) length of the next burst, but the classification of a process as CPU- or I/O-bound is itself often derived from the historical pattern of its past burst lengths — the same exponential-averaging machinery serves both purposes.
Why does aging solve the starvation problem, and why isn't it built into pure SJF/SRTF by definition?
Aging works by gradually increasing a waiting process's effective priority the longer it sits in the ready queue, eventually forcing it to the front regardless of its burst length. It is not part of SJF/SRTF by definition because the algorithm's entire logic is "always prefer the objectively shortest job" — adding aging means sometimes deliberately running a longer job, which contradicts the pure optimality criterion for average waiting time. Aging is therefore a designed compromise: it sacrifices some of the mathematically optimal average waiting time in exchange for a fairness guarantee (bounded maximum wait), which is exactly the kind of tradeoff Priority Scheduling formalizes explicitly.
How does SJF relate to Priority Scheduling and Round-Robin Scheduling?
SJF is a special case of Priority Scheduling where a process's priority is defined as the inverse of its (predicted) burst time — shorter jobs simply have higher priority. This reframing shows SJF's starvation problem is really priority scheduling's general starvation problem in disguise, solvable by the same aging techniques. Round-Robin Scheduling takes the opposite philosophy entirely: rather than trying to predict and exploit burst-time differences, it gives every process a fixed time quantum in rotation, sacrificing SJF's optimal average waiting time to guarantee bounded response time and eliminate starvation by construction.
What complexity would a naive (non-heap) SJF/SRTF implementation have, and why use a heap instead?
A naive implementation rescans the entire ready queue to find the minimum burst/remaining-time at every decision point, costing per decision and overall for scheduling events — acceptable for small classroom examples but poor for a real scheduler with thousands of processes. Using a min-heap keyed on (remaining) burst time reduces each insertion and extract-min to , giving total across all arrival and completion events, which is asymptotically the same cost as simply sorting the jobs once — the fundamental complexity floor for any comparison-based scheduling of jobs.