Skip to content
About
Main content

FIFO versus SRPT, on heavy-tailed and on steady job sizes

Simulate a modelNeeds Python

Problem

Shortest remaining processing time () always runs the job closest to finishing, preempting longer ones. It minimizes mean , and the usual objection is that it starves big jobs. Whether it does depends on the size distribution. When sizes are , a few huge jobs make up most of the work: under everything 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 mostly delay whichever job is unlucky.

Claim to test: "shortest-first starves big jobs" is true only when job sizes have low . Simulate one server at 90% load. Compare FIFO and preemptive SRPT with (a) 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, and the largest jobs, with heavy-tailed and with steady sizes?


Predict

With steady job sizes, how do the largest 10% of jobs fare under SRPT compared with FIFO?

Run it

  1. 1Install uv once. It fetches Python and the lab's packages for you.

    Terminal
    curl -LsSf https://astral.sh/uv/install.sh | sh
  2. 2Get the script. It is the whole lab: read it, then download it.

    fifo-versus-srpt-on-heavy-tailed.py67 linesDownload
    View 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)
    
  3. 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
Knobs
Knob · defaultWhat it changes
--jobs200,000 Jobs per run.
--load0.9 Server load.
--seeds5 Runs per size distribution.

Our run

Median of 5 seeds

Median of 5 seeds
SizesPolicyMean flowp99Largest 10%, mean
heavy-tailedFIFO64.174269.1
heavy-tailedSRPT6.6778.241.9
steadyFIFO5.4321.65.57
steadySRPT5.1667.923.4
Reference runApple M2 · 8 cores · 8 GB
Output · uv run fifo-versus-srpt-on-heavy-tailed.py
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 seeds
macOS 26.6.2 · Python 3.13.3 · run 2026-10-20

What 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 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.