Find the cost of ordering
Measure your machineNeeds Python
Problem
A backfill 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
About 60 s, about 15 s, and about 60 s. The difference between rows 2 and 3 is the exact price of ordering. In the ordered case, one failing interval blocks all later intervals.
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
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)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
| Knob · default | What 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
| Mode | Failure | Wall time | Intervals succeeded |
|---|---|---|---|
| sequential | none | 60.3 s | 60 |
| parallel | none | 15.1 s | 60 |
| depends_on_past | none | 60.2 s | 60 |
| parallel | interval 10 fails | 15.1 s | 59 |
| depends_on_past | interval 10 fails | 11.2 s | 10 |
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 10What 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 blast radius, 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.