Skip to content
About
Main content

Find the cost of ordering

Measure your machineNeeds Python

Problem

A of independent intervals can run in parallel; one that must run in order (each interval depends on the previous one, depends_on_past) cannot, however many workers it has. Ordering also changes failure: an independent interval that fails affects only itself, while in an ordered chain it blocks everything after it.

Take a workload where each interval takes 1 s (a sleep). Run 60 intervals sequentially, then in parallel with 4 workers, then with 4 workers but a depends_on_past-style rule that forces order. Record wall-clock times. Then make one interval fail and count the intervals that complete in each parallel mode.

The question. What does ordering cost in wall time, and how many intervals does one failure block?


Predict

With four workers under depends_on_past, how long do 60 one-second intervals take?

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.

    find-the-cost-of-ordering.py57 linesDownload
    View the code
    find-the-cost-of-ordering.py
    # /// script
    # requires-python = ">=3.10"
    # ///
    # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT
    # https://thedataengineering.com/lab-code/LICENSE.txt
    """Find the cost of ordering.
    
    60 intervals that take 1 s each (a sleep). Run them sequentially, in parallel
    on 4 workers, and on 4 workers with a depends_on_past rule (an interval may
    start only after the previous one succeeded). Then make interval 10 fail and
    count the intervals that run in each parallel mode.
    """
    import argparse, json, threading, time
    from concurrent.futures import ThreadPoolExecutor
    
    p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    p.add_argument("--intervals", type=int, default=60, help="Intervals.")
    p.add_argument("--seconds", type=float, default=1.0, help="Seconds per interval.")
    p.add_argument("--workers", type=int, default=4, help="Workers.")
    p.add_argument("--fail", type=int, default=10, help="Interval that fails in the failure runs.")
    p.add_argument("--json", help="Also write the results to this JSON file.")
    args = p.parse_args()
    
    def work(i, fail):
        time.sleep(args.seconds)
        if i == fail: raise RuntimeError(f"interval {i} failed")
    
    def run(mode, fail=-1):
        t0 = time.perf_counter(); ran = 0
        if mode == "sequential":
            for i in range(args.intervals):
                try: work(i, fail); ran += 1
                except RuntimeError: break
        elif mode == "parallel":
            with ThreadPoolExecutor(args.workers) as ex:
                futs = [ex.submit(work, i, fail) for i in range(args.intervals)]
                ran = sum(1 for f in futs if f.exception() is None)
        else:                                                              # depends_on_past: interval i waits for i - 1 to succeed
            ok = [threading.Event() for _ in range(args.intervals)]; failed = threading.Event(); count = [0]; lock = threading.Lock()
            def task(i):
                if i > 0:
                    while not ok[i - 1].wait(0.01):
                        if failed.is_set(): return                         # blocked forever: give up
                try: work(i, fail); ok[i].set()
                except RuntimeError: failed.set(); return
                with lock: count[0] += 1
            with ThreadPoolExecutor(args.workers) as ex: list(ex.map(task, range(args.intervals)))
            ran = count[0]
        return {"mode": mode, "fail": "interval %d fails" % fail if fail >= 0 else "none", "wall_s": time.perf_counter() - t0, "succeeded": ran}
    
    rows = [run("sequential"), run("parallel"), run("depends_on_past"), run("parallel", args.fail), run("depends_on_past", args.fail)]
    for r in rows: print(f"{r['mode']:<16} failure: {r['fail']:<18} wall {r['wall_s']:5.1f} s   intervals succeeded {r['succeeded']}")
    
    if args.json:
        with open(args.json, "w") as f:
            json.dump({"tables": {"runs": rows}}, 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 find-the-cost-of-ordering.py
    uv run find-the-cost-of-ordering.py --help     # every knob
Knobs
Knob · defaultWhat it changes
--intervals60 Intervals.
--seconds1 Seconds per interval.
--workers4 Workers.
--fail10 Interval that fails in the failure runs.

Our run

60 intervals of 1 s

60 intervals of 1 s
ModeFailureWall timeIntervals succeeded
sequentialnone60.3 s60
parallelnone15.1 s60
depends_on_pastnone60.2 s60
parallelinterval 10 fails15.1 s59
depends_on_pastinterval 10 fails11.2 s10
Reference runApple M2 · 8 cores · 8 GB
Output · uv run find-the-cost-of-ordering.py
sequential       failure: none               wall  60.3 s   intervals succeeded 60
parallel         failure: none               wall  15.1 s   intervals succeeded 60
depends_on_past  failure: none               wall  60.2 s   intervals succeeded 60
parallel         failure: interval 10 fails  wall  15.1 s   intervals succeeded 59
depends_on_past  failure: interval 10 fails  wall  11.2 s   intervals succeeded 10
macOS 26.6.2 · Python 3.13.3 · run 2026-10-20

What we found

Sequential took 60.3 s, four workers 15.1 s, and four workers under depends_on_past 60.2 s: the ordering rule turned the extra workers into idle ones. With interval 10 failing, the parallel run completed 59 intervals and the ordered run 10: everything after the failure was blocked.


Read it

Ordering is a dependency you pay for twice: in time, because it serialises work, and in , because a failure blocks the rest of the chain. Use it only where an interval truly reads the previous one's output (running totals, slowly changing state), and otherwise make intervals independent.