FIFO versus SRPT, on heavy-tailed and on steady job sizes
Simulate a modelNeeds Python
Problem
Shortest remaining processing time (SRPT) always runs the job closest to finishing, preempting longer ones. It minimizes mean flow time, and the usual objection is that it starves big jobs. Whether it does depends on the size distribution. When sizes are heavy-tailed, a few huge jobs make up most of the work: under FIFO everything queues behind them, including other big jobs, and moving small jobs out of the way costs the big ones little. When sizes are nearly equal, there is little to reorder and SRPT's preemptions mostly delay whichever job is unlucky.
Claim to test: "shortest-first starves big jobs" is true only when job sizes have low variance. Simulate one server at 90% load. Compare FIFO and preemptive SRPT with (a) heavy-tailed sizes and (b) almost constant sizes. Report the mean flow time, the p99, and the mean flow time of the largest 10% of jobs, over five seeds (heavy-tailed simulations are noisy).
The question. Does SRPT help or hurt the mean, the tail and the largest jobs, with heavy-tailed and with steady sizes?
Predict
With steady sizes the two policies are close (SRPT gives little gain, and large jobs can be a little worse). With heavy-tailed sizes SRPT's mean is much lower, and the mean flow time of the largest 10% is usually also lower, or at worst similar.
Run it
1Install uv once. It fetches Python and the lab's packages for you.
Terminal curl -LsSf https://astral.sh/uv/install.sh | sh2Get the script. It is the whole lab: read it, then download it.
View the codeHide the code
fifo-versus-srpt-on-heavy-tailed.py # /// script # requires-python = ">=3.10" # /// # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT # https://thedataengineering.com/lab-code/LICENSE.txt """FIFO against SRPT on heavy-tailed and steady job sizes. One server at 90% load, 200,000 jobs, sizes either heavy-tailed (Pareto, shape 1.8) or almost constant. FIFO against preemptive shortest remaining processing time (SRPT): mean flow time, p99, and the mean flow time of the largest 10% of jobs, over several seeds. """ import argparse, heapq, json, random, statistics p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) p.add_argument("--jobs", type=int, default=200_000, help="Jobs per run.") p.add_argument("--load", type=float, default=0.9, help="Server load.") p.add_argument("--seeds", type=int, default=5, help="Runs per size distribution.") p.add_argument("--json", help="Also write the results to this JSON file.") args = p.parse_args() def gen(n, load, size_fn): sizes = [size_fn() for _ in range(n)] rate = load / (sum(sizes) / n) # arrival rate that gives this load t, jobs = 0.0, [] for s in sizes: t += random.expovariate(rate); jobs.append((t, s)) return jobs def fifo(jobs): free, out = 0.0, [] for a, s in jobs: start = max(a, free); free = start + s; out.append((s, free - a)) # (size, flow time) return out def srpt(jobs): t, i, heap, out, n = 0.0, 0, [], [], len(jobs) while i < n or heap: if not heap: t = max(t, jobs[i][0]) while i < n and jobs[i][0] <= t: a, s = jobs[i]; heapq.heappush(heap, (s, a, s)); i += 1 rem, arr, size = heapq.heappop(heap) nxt = jobs[i][0] if i < n else float("inf") if t + rem <= nxt: t += rem; out.append((size, t - arr)) else: heapq.heappush(heap, (rem - (nxt - t), arr, size)); t = nxt return out def stats(res): res.sort(); flows = sorted(f for _, f in res); big = [f for _, f in res[int(0.9 * len(res)):]] return statistics.mean(flows), flows[int(0.99 * len(flows))], statistics.mean(big) rows = [] for label, fn in (("heavy-tailed", lambda: random.paretovariate(1.8)), ("steady", lambda: random.uniform(0.9, 1.1))): for seed in range(args.seeds): random.seed(seed); jobs = gen(args.jobs, args.load, fn) for pol, f in (("FIFO", fifo), ("SRPT", srpt)): m, p99, big = stats(f(jobs)); rows.append({"sizes": label, "seed": seed, "policy": pol, "mean": m, "p99": p99, "big10_mean": big}) summary = [] for label in ("heavy-tailed", "steady"): for pol in ("FIFO", "SRPT"): rs = [r for r in rows if r["sizes"] == label and r["policy"] == pol] summary.append({"sizes": label, "policy": pol, **{k: statistics.median(r[k] for r in rs) for k in ("mean", "p99", "big10_mean")}}) big_better = sum(1 for s in range(args.seeds) if next(r for r in rows if r["sizes"] == "heavy-tailed" and r["seed"] == s and r["policy"] == "SRPT")["big10_mean"] <= next(r for r in rows if r["sizes"] == "heavy-tailed" and r["seed"] == s and r["policy"] == "FIFO")["big10_mean"]) for r in summary: print(f"{r['sizes']:<13} {r['policy']}: mean={r['mean']:8.2f} p99={r['p99']:9.2f} largest 10% mean={r['big10_mean']:9.2f} (median of {args.seeds} seeds)") print(f"heavy-tailed: SRPT's largest 10% did as well or better than FIFO's in {big_better} of {args.seeds} seeds") if args.json: with open(args.json, "w") as f: json.dump({"tables": {"policies": summary, "seeds": rows, "summary": [{"big_better": big_better, "seeds": args.seeds}]}}, f)3Run it in the folder you saved it to, with the defaults first. Then change a knob from the table below and run it again.
Terminal uv run fifo-versus-srpt-on-heavy-tailed.py uv run fifo-versus-srpt-on-heavy-tailed.py --help # every knob
| Knob · default | What it changes |
|---|---|
| --jobs200,000 | Jobs per run. |
| --load0.9 | Server load. |
| --seeds5 | Runs per size distribution. |
Our run
Median of 5 seeds
| Sizes | Policy | Mean flow | p99 | Largest 10%, mean |
|---|---|---|---|---|
| heavy-tailed | FIFO | 64.1 | 742 | 69.1 |
| heavy-tailed | SRPT | 6.67 | 78.2 | 41.9 |
| steady | FIFO | 5.43 | 21.6 | 5.57 |
| steady | SRPT | 5.16 | 67.9 | 23.4 |
heavy-tailed FIFO: mean= 64.09 p99= 742.06 largest 10% mean= 69.13 (median of 5 seeds)
heavy-tailed SRPT: mean= 6.67 p99= 78.25 largest 10% mean= 41.94 (median of 5 seeds)
steady FIFO: mean= 5.43 p99= 21.63 largest 10% mean= 5.57 (median of 5 seeds)
steady SRPT: mean= 5.16 p99= 67.86 largest 10% mean= 23.41 (median of 5 seeds)
heavy-tailed: SRPT's largest 10% did as well or better than FIFO's in 5 of 5 seedsWhat we found
With heavy-tailed sizes SRPT cut the mean flow time from 64.1 to 6.67 and the p99 from 742 to 78.2, and the largest 10% did better too (41.9 against 69.1), in 5 of 5 seeds. With steady sizes the means were close (5.43 and 5.16), and the largest jobs were not "a little" worse but several times worse under SRPT (23.4 against 5.57), with a p99 to match.
Read it
The starvation claim is true in exactly the case where SRPT has nothing to offer: equal sizes, where reordering gains little and each preemption delays someone. With heavy-tailed sizes, the case most data systems live in, shortest-first improves the mean, the tail and the big jobs together. Know your size distribution before choosing the policy.