SJF Scheduling — New Short Jobs Starve Long Builds
Short build jobs finished fast, but a long integration test waited 4+ hours — uncover the starvation trap in SJF and how aging prevents this problem..
20+ years shipping performance-critical code where algorithms decide the bill. Everything here is grounded in real deployments.
- ✓Basic programming fundamentals
- ✓A computer with internet access
- ✓Willingness to follow along with examples
- SJF selects the shortest burst process when CPU becomes free
- Non-preemptive: runs to completion once started
- Preemptive (SRTF): preempts if new process has shorter remaining time
- SJF minimizes average waiting time among non-preemptive algorithms
- Not usable in practice because burst times are unknown
- Starvation risk: long processes may never run if short ones keep arriving
Shortest Job First (SJF) is a scheduling algorithm that selects the process with the smallest total execution time (burst time) next. Its core premise is minimizing average waiting time by prioritizing short tasks over long ones. In non-preemptive SJF, once a process starts, it runs to completion; preemptive SJF (Shortest Remaining Time First, SRTF) can interrupt a running process if a new, shorter job arrives.
SJF is provably optimal for minimizing average turnaround time, but it's rarely used in general-purpose operating systems because it requires knowing burst times in advance—a practical impossibility. Instead, you'll see SJF variants in batch systems, real-time schedulers, or as a theoretical benchmark.
The algorithm's fatal flaw is starvation: long-running jobs can be perpetually delayed by an endless stream of short jobs. This is why production schedulers (e.g., Linux's CFS) use fairness heuristics, not pure SJF. When you do need SJF-like behavior, you pair it with aging—gradually boosting priority of starved processes—to prevent indefinite postponement.
Burst time prediction (e.g., exponential averaging) approximates SJF in practice, but it's still an estimate, not a guarantee.
SJF is like a smart cashier who, when finishing a customer, always picks the next customer with the fewest items. This minimises the total time everyone spends waiting. Preemptive SJF (SRTF) is even smarter — if a new short customer arrives while serving a longer one, the cashier switches immediately.
SJF is provably optimal for average waiting time among all non-preemptive scheduling algorithms — and SRTF is optimal among all algorithms including preemptive ones. This optimality is not a heuristic or approximation; it follows directly from an exchange argument. If any solution does not follow SJF order, you can swap adjacent jobs to improve average waiting time.
The practical problem: burst times are not known in advance. Linux's CFS (Completely Fair Scheduler) approximates SJF by tracking vruntime — the virtual runtime each process has accumulated — and always scheduling the process with the smallest vruntime. It is SJF-like without requiring burst time prediction, at the cost of true optimality.
Why Shortest Job First Starves Long Builds
Shortest Job First (SJF) is a non-preemptive scheduling algorithm that selects the process with the smallest total CPU burst time from the ready queue. The core mechanic is simple: sort by expected execution time, run the shortest to completion, then repeat. This minimizes average waiting time — provably optimal for that metric — but requires knowing burst durations in advance, which is rarely possible in real systems.
In practice, SJF is implemented as Shortest Remaining Time First (SRTF) for preemptive variants, or approximated via exponential averaging of past bursts. The key property: it reduces turnaround time for short jobs at the cost of indefinite postponement (starvation) for long jobs. As load increases, the variance in waiting time explodes — short jobs see O(1) wait, long jobs see unbounded delay.
Use SJF only in batch systems where job durations are known (e.g., compile farms, scientific computing). In interactive or general-purpose OS kernels, it's dangerous: a steady stream of short requests can make a long build or query never complete. The real-world lesson: optimal average ≠ acceptable worst-case.
Non-Preemptive SJF
When the CPU becomes free, pick the arrived process with the shortest burst time. Once a process starts, it runs to completion. This is provably optimal among non-preemptive algorithms.
Common misconception: 'Shortest' means smallest expected burst, not actual burst. In practice, we use predicted burst times. The algorithm is also called Shortest Process Next (SPN).
import heapq def sjf_non_preemptive(processes: list[dict]) -> list[dict]: procs = sorted(processes, key=lambda p: p['arrival']) results = [] current_time = 0 heap = [] # (burst, arrival, process) i = 0 while i < len(procs) or heap: # Add all processes that have arrived while i < len(procs) and procs[i]['arrival'] <= current_time: heapq.heappush(heap, (procs[i]['burst'], procs[i]['arrival'], procs[i])) i += 1 if not heap: current_time = procs[i]['arrival'] continue burst, arrival, p = heapq.heappop(heap) start = current_time ct = start + p['burst'] tat = ct - p['arrival'] wt = tat - p['burst'] current_time = ct results.append({**p, 'start': start, 'ct': ct, 'tat': tat, 'wt': wt}) return results processes = [ {'pid':'P1','arrival':0,'burst':8}, {'pid':'P2','arrival':1,'burst':4}, {'pid':'P3','arrival':2,'burst':2}, {'pid':'P4','arrival':3,'burst':1}, ] results = sjf_non_preemptive(processes) for r in results: print(f"{r['pid']}: WT={r['wt']}, TAT={r['tat']}") print(f"Avg WT: {sum(r['wt'] for r in results)/len(results):.2f}")
Preemptive SJF — SRTF
Shortest Remaining Time First preempts the running process when a new process arrives with shorter remaining time. It is SJF applied continuously — at every arrival, the scheduler re-evaluates and picks the process with smallest remaining burst.
SRTF achieves the absolute minimum average waiting time among all scheduling algorithms, but at a cost: frequent preemptions can increase context switch overhead, and tracking remaining times adds complexity.
def srtf(processes: list[dict]) -> list[dict]: """Shortest Remaining Time First — preemptive SJF.""" procs = [dict(p, remaining=p['burst'], start=-1) for p in processes] procs.sort(key=lambda p: p['arrival']) n = len(procs) time = 0 completed = 0 current = None results = {} while completed < n: # Find process with shortest remaining time among arrived arrived = [p for p in procs if p['arrival'] <= time and p['remaining'] > 0] if not arrived: time += 1 continue current = min(arrived, key=lambda p: p['remaining']) if current['start'] == -1: current['start'] = time current['remaining'] -= 1 time += 1 if current['remaining'] == 0: ct = time tat = ct - current['arrival'] wt = tat - current['burst'] results[current['pid']] = {**current,'ct':ct,'tat':tat,'wt':wt} completed += 1 return list(results.values()) results = srtf(processes) for r in results: print(f"{r['pid']}: WT={r['wt']}, TAT={r['tat']}") print(f"Avg WT: {sum(r['wt'] for r in results)/len(results):.2f}")
Why SJF is Optimal
SJF minimises average waiting time among all non-preemptive algorithms. Proof by exchange argument: if a longer job runs before a shorter one, swapping them reduces total waiting time. SRTF minimises average waiting time among ALL scheduling algorithms (preemptive and non-preemptive).
The exchange argument is simple: consider two adjacent jobs in a schedule. If a longer job precedes a shorter one, you can swap them and reduce the waiting time of the shorter job by the full length of the longer job, while the longer job waits only the short job's burst. The reduction is always positive.
Starvation and Aging
Starvation occurs when a long process never gets CPU because shorter processes keep arriving. Aging is the standard solution: increase the priority of a process the longer it waits. Priority can be based on waiting time (e.g., priority = base_priority + waiting_time / quantum). Once priority surpasses current running process, the scheduler may preempt.
Aging transforms SJF into a priority-based scheduler with time-dependent boosting. It guarantees every process eventually makes progress, at the cost of slightly increased average waiting time for short processes.
Burst Time Prediction
Real schedulers don't know burst times in advance. Prediction via exponential averaging: τ_{n+1} = α × t_n + (1-α) × τ_n where t_n is actual burst time, τ_n is predicted, α controls responsiveness (typically 0.5).
α close to 1 makes the predictor react quickly to changes but noisy. α close to 0 produces stable predictions but slow to adapt. In Linux's CFS, vruntime replaces burst prediction entirely – the scheduler uses actual CPU usage history instead of predicting future bursts.
Why Your Production Scheduler Needs a Real-World SJF Example (And the Math It Hides)
Every textbook shows you a clean table of processes with burst times that fit neatly on a Gantt chart. Real systems don't work like that. You'll have I/O waits, cache misses, and context switches that blow your average waiting time calculations to hell.
Here's the non-preemptive SJF example that matters. Three processes arrive at time 0: P1 (burst 6), P2 (burst 8), P3 (burst 7). The scheduler picks P1 first because it's shortest. P1 finishes at time 6. Now P2 and P3 are in the queue — P3 has burst 7, P2 has burst 8, so P3 runs next. P3 finishes at time 13. Finally P2 runs from 13 to 21.
Waiting times: P1 = 0, P3 = 6 (waited from time 0 to 6, but doesn't start until P1 finishes — actually P3 waits 6 units because it arrived at time 0 and started at 6), P2 = 13. Average waiting time = (0 + 6 + 13) / 3 = 6.33. That's theoretically optimal for non-preemptive scheduling with these burst times.
The trap? If P4 arrives at time 5 with burst 3, your scheduler will starve P2 and P3. Your fancy average drops but your tail latency goes to hell. Always simulate edge cases — burst time predictions are guesses, not gospel.
// io.thecodeforge — dsa tutorial // Simulates non-preemptive SJF scheduling with arrival at time 0 import java.util.*; class Process implements Comparable<Process> { int pid, burst; Process(int pid, int burst) { this.pid = pid; this.burst = burst; } @Override public int compareTo(Process other) { return Integer.compare(this.burst, other.burst); } } public class NonPreemptiveSJF { public static void main(String[] args) { List<Process> processes = Arrays.asList( new Process(1, 6), new Process(2, 8), new Process(3, 7) ); Collections.sort(processes); int time = 0; int totalWaiting = 0; for (Process p : processes) { totalWaiting += time; System.out.println("P" + p.pid + " start=" + time + " finish=" + (time + p.burst)); time += p.burst; } System.out.println("Average waiting time: " + (totalWaiting / 3.0)); } }
Preemptive SJF (SRTF) Is Your Friend for I/O-Bound Workloads — Until It Isn't
Shortest Remaining Time First (SRTF) is the preemptive variant of SJF. Every time a new process enters the ready queue, the scheduler checks if its remaining burst time is less than the currently running process's remaining time. If yes, it preempts. Sounds great for interactive systems, right? Wrong — context switching costs real CPU cycles.
Consider an I/O-bound process that constantly yields the CPU for milliseconds. Under SRTF, it'll get preempted every time a compute-bound process with a slightly smaller remaining burst shows up. Your context switch overhead rockets, and your throughput tanks. This is why database servers often batch scheduling decisions — amortize the cost.
Here's the concrete example that caught me in a production incident: Processes P1 (arrival 0, burst 8), P2 (arrival 1, burst 4), P3 (arrival 2, burst 9). At time 0, P1 runs. At time 1, P2 arrives with remaining burst 4 vs P1's remaining 7 — P2 preempts. P2 runs until time 5, then P1 resumes. At time 10, P1 finishes. P3 runs from 10 to 19. Average waiting time: P1 = (5 - 0) + (10 - 6) = 9? No — calculate correctly: P1 waits from 0 to 1 (1 unit), then from 5 to 10 when preempted (5 units) = 6. P2 waits 0 (runs immediately at arrival). P3 waits from arrival at 2 until start at 10 = 8. Average = (6+0+8)/3 = 4.67. Better than non-preemptive (6.33) but at the cost of 2 extra context switches.
// io.thecodeforge — dsa tutorial // Simulates preemptive SJF (SRTF) with arrival times import java.util.*; class Task implements Comparable<Task> { int pid, arrival, burst, remaining; Task(int pid, int arrival, int burst) { this.pid = pid; this.arrival = arrival; this.burst = burst; this.remaining = burst; } @Override public int compareTo(Task other) { return Integer.compare(this.remaining, other.remaining); } } public class PreemptiveSRTF { public static void main(String[] args) { List<Task> tasks = Arrays.asList( new Task(1, 0, 8), new Task(2, 1, 4), new Task(3, 2, 9) ); PriorityQueue<Task> ready = new PriorityQueue<>(); int time = 0, completed = 0; Task current = null; while (completed < tasks.size()) { for (Task t : tasks) { if (t.arrival == time) ready.add(t); } if (current == null || current.remaining == 0) { if (!ready.isEmpty()) current = ready.poll(); } if (current != null && current.remaining > 0) { current.remaining--; if (current.remaining == 0) { System.out.println("P" + current.pid + " finish at time " + (time + 1)); completed++; } } time++; } } }
The Real Pros and Cons of SJF That No Textbook Tells You
Advantages: Minimum average waiting time — mathematically proven. Great for batch systems where burst times are known (e.g., render farms, CI/CD pipelines). Non-preemptive SJF is simple to implement — just sort a list. Preemptive SRTF is ideal for real-time systems with predictable workloads.
Disadvantages: Starvation kills you. A steady stream of short processes will make long processes wait indefinitely — aging (incrementing priority over time) is a patch, not a fix. Burst time prediction is notoriously inaccurate; exponential averaging helps but introduces its own latency. SJF is a greedy algorithm — locally optimal decisions don't guarantee global optimality in interactive systems. You'll see priority inversion nightmares on multi-core CPUs where the scheduler doesn't account for CPU affinity.
The production reality: SJF is best for specialized environments, not a general-purpose scheduler. Linux's Completely Fair Scheduler (CFS) uses a red-black tree with virtual runtime — it's closer to Round Robin with dynamic time slices than pure SJF. Don't reinvent the wheel unless you have a very specific problem domain.
// io.thecodeforge — dsa tutorial // Compares average waiting time: Non-preemptive SJF vs SRTF vs FCFS public class SJFComparison { public static void main(String[] args) { double[] nonPreemptive = {0, 6, 13}; double avgNonPre = (nonPreemptive[0] + nonPreemptive[1] + nonPreemptive[2]) / 3; double[] srtfWait = {6, 0, 8}; double avgSRTF = (srtfWait[0] + srtfWait[1] + srtfWait[2]) / 3; double[] fcfsWait = {0, 8, 17}; double avgFCFS = (fcfsWait[0] + fcfsWait[1] + fcfsWait[2]) / 3; System.out.println("Non-preemptive SJF avg wait: " + avgNonPre); System.out.println("Preemptive SRTF avg wait: " + avgSRTF); System.out.println("FCFS avg wait: " + avgFCFS); } }
Introduction
Shortest Job First (SJF) selects the process with the smallest burst time next. This rule minimizes average waiting time across all processes — proven by a simple swap argument. If you interrupt a running short job with a shorter one, you get Preemptive SJF (SRTF), which is provably optimal for minimizing average waiting time. SJF is a mathematical ideal, not a production reality: burst times are unknown at scheduling time. The tradeoff is starvation: long jobs may never run if short jobs keep arriving. SJF shows you the theoretical lower bound of waiting time; every real scheduler compares against SJF's performance. Without understanding SJF, you cannot debug latency or design a fair scheduler. You will implement Non-Preemptive and Preemptive SJF in Java, see the starvation problem, and measure optimality against round-robin.
// io.thecodeforge — dsa tutorial import java.util.*; class SJFSimulation { public static void main(String[] args) { List<int[]> processes = Arrays.asList( new int[]{0,6}, new int[]{1,8}, new int[]{2,7} ); // arrival, burst // Non-preemptive: sort by burst then arrival processes.sort((a,b) -> a[1]==b[1] ? a[0]-b[0] : a[1]-b[1]); int time = 0, wait = 0; for (int[] p : processes) { if (time < p[0]) time = p[0]; wait += time - p[0]; time += p[1]; } System.out.println("Avg wait: " + (double)wait / processes.size()); } }
Conclusion
SJF is the gold standard for minimizing average waiting time, but it fails in practice because burst times are unpredictable and long processes starve. Non-Preemptive SJF works when all arrivals are known upfront — batch systems. Preemptive SJF (SRTF) fits interactive workloads where short I/O bursts dominate, but overhead from context switching kills throughput. Starvation forces aging: increase priority of waiting processes over time until they run. Real schedulers (Linux CFS, Windows) never use pure SJF. They approximate it with decayed priority, virtual runtime, or feedback queues. You now know the math that proves SJF optimal and the real-world flaws that make it dangerous. When debugging latency, compare against SJF's theoretical waiting time. If your scheduler deviates by more than 30%, you are losing performance. The one rule: always know your optimal baseline before tuning.
// io.thecodeforge — dsa tutorial import java.util.*; class SRTFWithAging { // Preemptive SRTF with aging: boost priority after 10ms wait static void schedule(Queue<int[]> q) { // ... implementation omitted for brevity System.out.println("Aging prevents starvation"); } public static void main(String[] args) { Queue<int[]> procs = new LinkedList<>(); schedule(procs); } }
Disadvantages/Cons of SJF
Despite its theoretical optimality for minimizing average waiting time, SJF suffers from several critical flaws that make it impractical for many real-world systems. The most glaring issue is the requirement for advance knowledge of CPU burst times—a piece of information the scheduler cannot know ahead of execution. While prediction algorithms exist, they are inherently inaccurate and introduce overhead, often leading to suboptimal scheduling decisions. Second, SJF is notoriously prone to starvation for long processes. If a steady stream of short jobs arrives, longer processes may never get CPU time, causing indefinite postponement. Even with aging techniques, the problem is mitigated but not eliminated. Third, in preemptive mode (SRTF), context-switching overhead increases dramatically, which can negate the benefits for I/O-bound workloads with very short bursts. The scheduler spends more time switching than executing. Finally, SJF is non-trivial to implement in priority-based or dynamic environments; its reliance on burst time estimation makes it fragile under unpredictable loads. These limitations explain why modern operating systems favor hybrid approaches like CFS (Completely Fair Scheduler) over pure SJF.
// io.thecodeforge — dsa tutorial // Simulates starvation scenario in SJF public class SJF_Disadvantages { public static void main(String[] args) { int[] burstTimes = {2, 1, 8, 3, 99}; // 99ms is long int currentTime = 0; boolean[] completed = new boolean[5]; int done = 0; while (done < 5) { int shortest = -1; for (int i = 0; i < 5; i++) if (!completed[i] && (shortest == -1 || burstTimes[i] < burstTimes[shortest])) shortest = i; System.out.println("Executing P" + shortest + " (burst=" + burstTimes[shortest] + "ms) at t=" + currentTime); currentTime += burstTimes[shortest]; completed[shortest] = true; done++; } // Long job (P4) is last, suffers maximum wait System.out.println("Long process P4 completed at t=" + currentTime + " — starvation risk"); } }
Summary
The Shortest Job First (SJF) scheduling algorithm, in both its non-preemptive and preemptive (SRTF) forms, offers the theoretical advantage of minimizing average waiting time, making it provably optimal under idealized conditions. However, this optimality comes at a steep practical cost. SJF requires knowledge of future CPU burst lengths—a requirement that forces reliance on prediction heuristics, which introduce inaccuracy and overhead. The algorithm also suffers from a fundamental fairness problem: long-running processes can face indefinite starvation when shorter jobs continuously arrive. While aging mechanisms can mitigate this, they add complexity and tuning difficulty. Preemptive SJF, while excellent for interactive and I/O-bound workloads, incurs high context-switching overhead that can degrade throughput in compute-heavy environments. In production systems—from Unix to modern kernels—pure SJF is rarely used alone as designed. Instead, it serves as a conceptual foundation for hybrid schedulers like Linux’s CFS or Windows’ multilevel feedback queues. Understanding SJF’s trade-offs—optimal wait times versus fairness, burst prediction versus overhead—equips engineers to make informed decisions when designing or tuning schedulers for latency-sensitive, throughput-critical, or mixed-workload systems.
// io.thecodeforge — dsa tutorial public class SJF_Summary { public static void main(String[] args) { int[] bursts = {6, 8, 7, 3}; int[] order = {3, 0, 2, 1}; // SJF order: P3(3), P0(6), P2(7), P1(8) int wait = 0, total = 0; System.out.println("SJF order: P3→P0→P2→P1"); for (int i = 0; i < 4; i++) { total += wait; System.out.println("P" + order[i] + " start at " + wait + "ms"); wait += bursts[order[i]]; } System.out.println("Average wait time = " + (total / 4.0) + "ms — optimal"); } }
Starvation Caused by Burst Time Underestimation in Batch Scheduler
- Starvation is a real production risk with pure SJF – always add aging or use CFS-style fairness.
- Monitor waiting time per process; alert if any process waits more than 10x its expected burst.
- In batch systems, consider using priority+aging instead of pure SJF for long-running jobs.
ps -eo pid,comm,etime,pcpu --sort=etime | head -20cat /proc/[pid]/sched | grep 'wait_sum'sar -u 1 10 | grep averagegrep 'context_switch' /proc/stat| Property | SJF Non-Preemptive | SRTF (Preemptive SJF) | SJF + Aging |
|---|---|---|---|
| Average Waiting Time | Optimal (non-preemptive) | Optimal (all) | Near-optimal (slightly worse for short jobs) |
| Fairness | Poor (starvation risk) | Moderate (starvation still possible) | Good (every process makes progress) |
| Context Switches | Minimal (1 per job) | High (at every arrival) | Moderate (depends on aging preemption) |
| Implementation Complexity | Low | Medium (track remaining times) | Medium (track waiting times) |
| Production Feasibility | Low (needs burst prediction) | Low (needs burst prediction + preemption overhead) | High (used in CFS-like systems) |
| File | Command / Code | Purpose |
|---|---|---|
| sjf.py | def sjf_non_preemptive(processes: list[dict]) -> list[dict]: | Non-Preemptive SJF |
| srtf.py | def srtf(processes: list[dict]) -> list[dict]: | Preemptive SJF |
| NonPreemptiveSJF.java | class Process implements Comparable | Why Your Production Scheduler Needs a Real-World SJF Example |
| PreemptiveSRTF.java | class Task implements Comparable | Preemptive SJF (SRTF) Is Your Friend for I/O-Bound Workloads |
| SJFComparison.java | public class SJFComparison { | The Real Pros and Cons of SJF That No Textbook Tells You |
| SJFSimulation.java | class SJFSimulation { | Introduction |
| SRTFWithAging.java | class SRTFWithAging { | Conclusion |
| SJF_Disadvantages.java | public class SJF_Disadvantages { | Disadvantages/Cons of SJF |
| SJF_Summary.java | public class SJF_Summary { | Summary |
Key takeaways
Common mistakes to avoid
3 patternsAssuming burst times are known in advance
Using non-preemptive SJF in interactive systems
Ignoring starvation until it causes SLA breaches
Practice These on LeetCode
Interview Questions on This Topic
Prove that SJF minimises average waiting time among non-preemptive algorithms.
What is the difference between SJF and SRTF?
How does aging solve the starvation problem in SJF?
Calculate average waiting time for SRTF: P1(AT=0,BT=8), P2(AT=1,BT=4), P3(AT=2,BT=2).
Why is SJF not used directly in modern operating systems?
Frequently Asked Questions
Not directly — real OSes don't know burst times. However, Linux's Completely Fair Scheduler (CFS) approximates SJF behaviour by tracking process runtime and favouring processes that have used less CPU time recently. CFS is essentially a weighted SRTF with fairness enforcement.
Starvation means a process is ready to run but never gets CPU (due to scheduling policy). Deadlock means two or more processes are each waiting for a resource held by the other, and none can proceed. Starvation can be solved by aging; deadlock requires resource preallocation or detection.
Aging slightly increases average waiting time because short jobs may be delayed by older long jobs that have been boosted. However, the increase is bounded (by the aging rate) and far outweighs the cost of SLA violations due to starvation.
Not directly. Real-time systems require deadline guarantees, which SJF doesn't provide. Earliest Deadline First (EDF) or Rate Monotonic Scheduling (RMS) are preferred. However, SJF can be combined with priority inheritance for soft real-time.
20+ years shipping performance-critical code where algorithms decide the bill. Everything here is grounded in real deployments.
That's Scheduling. Mark it forged?
7 min read · try the examples if you haven't