Throughput versus goodput past the knee
Simulate a modelNeeds Python
Problem
Past full load, a queue fills, and every request waits longer than the one before. Once the wait exceeds the client's timeout, the server is still busy, so throughput looks fine, but it is answering requests nobody is waiting for: goodput falls to zero. A bounded queue caps the wait so that served requests are still wanted; a deadline check at dequeue discards requests that have already expired instead of spending capacity on them.
Simulate a service that completes 100 requests per second, receives 150 per second, and whose clients give up after 2 seconds. Compare (a) an unbounded queue, (b) a bounded queue (200), (c) a bounded queue plus a deadline check at dequeue.
The question. What goodput does each queue deliver over a minute of overload?
Predict
The unbounded queue's goodput collapses toward zero after the first seconds, because every request is stale when served. Bounded plus the dequeue check stays near the capacity of 100 per second.
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
throughput-versus-goodput-past-the-knee.py # /// script # requires-python = ">=3.10" # /// # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT # https://thedataengineering.com/lab-code/LICENSE.txt """Throughput against goodput past the knee. A service completes 100 requests per second and receives 150; clients give up after 2 seconds. (a) an unbounded queue, (b) a queue bounded at 200, (c) the bounded queue plus a deadline check at dequeue. Goodput (answers delivered to clients still waiting) per second over a minute. """ import argparse, json from collections import deque p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) p.add_argument("--arrival", type=int, default=150, help="Requests per second.") p.add_argument("--capacity", type=int, default=100, help="Requests served per second.") p.add_argument("--timeout", type=int, default=2, help="Client timeout (s).") p.add_argument("--max-queue", type=int, default=200, help="Bound for the bounded queue.") p.add_argument("--seconds", type=int, default=60, help="Seconds simulated.") p.add_argument("--json", help="Also write the results to this JSON file.") args = p.parse_args() def sim(max_queue=None, check=False): q, good, per_sec = deque(), 0, [] for sec in range(args.seconds): g0 = good for _ in range(args.arrival): if max_queue is None or len(q) < max_queue: q.append(sec) # remember the arrival second for _ in range(args.capacity): while q: arr = q.popleft() if sec - arr > args.timeout: if check: continue # discard for free, try the next one break # capacity spent on a dead request good += 1; break per_sec.append(good - g0) return good / args.seconds, per_sec rows, timeline = [], [] for label, kw in (("unbounded", {}), (f"bounded ({args.max_queue})", {"max_queue": args.max_queue}), (f"bounded ({args.max_queue}) + dequeue check", {"max_queue": args.max_queue, "check": True})): g, ps = sim(**kw); rows.append({"queue": label, "goodput": g, "last_10s": sum(ps[-10:]) / 10}) timeline += [{"queue": label, "second": i, "goodput": v} for i, v in enumerate(ps)] for r in rows: print(f"{r['queue']:<34} goodput {r['goodput']:6.1f}/s over the minute, {r['last_10s']:6.1f}/s in the last 10 s") if args.json: with open(args.json, "w") as f: json.dump({"tables": {"queues": rows, "timeline": timeline}}, 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 throughput-versus-goodput-past-the-knee.py uv run throughput-versus-goodput-past-the-knee.py --help # every knob
| Knob · default | What it changes |
|---|---|
| --arrival150 | Requests per second. |
| --capacity100 | Requests served per second. |
| --timeout2 | Client timeout (s). |
| --max-queue200 | Bound for the bounded queue. |
| --seconds60 | Seconds simulated. |
Our run
Data table
| Second | series | Answers delivered in time (/s) |
|---|---|---|
| 0 | unbounded | 100 |
| 1 | unbounded | 100 |
| 2 | unbounded | 100 |
| 3 | unbounded | 100 |
| 4 | unbounded | 100 |
| 5 | unbounded | 100 |
| 6 | unbounded | 100 |
| 7 | unbounded | 50 |
| 8 | unbounded | 0 |
| 9 | unbounded | 0 |
| 10 | unbounded | 0 |
| 11 | unbounded | 0 |
| 12 | unbounded | 0 |
| 13 | unbounded | 0 |
| 14 | unbounded | 0 |
| 15 | unbounded | 0 |
| 16 | unbounded | 0 |
| 17 | unbounded | 0 |
| 18 | unbounded | 0 |
| 19 | unbounded | 0 |
| 20 | unbounded | 0 |
| 21 | unbounded | 0 |
| 22 | unbounded | 0 |
| 23 | unbounded | 0 |
| 24 | unbounded | 0 |
| 25 | unbounded | 0 |
| 26 | unbounded | 0 |
| 27 | unbounded | 0 |
| 28 | unbounded | 0 |
| 29 | unbounded | 0 |
| 30 | unbounded | 0 |
| 31 | unbounded | 0 |
| 32 | unbounded | 0 |
| 33 | unbounded | 0 |
| 34 | unbounded | 0 |
| 35 | unbounded | 0 |
| 36 | unbounded | 0 |
| 37 | unbounded | 0 |
| 38 | unbounded | 0 |
| 39 | unbounded | 0 |
| 40 | unbounded | 0 |
| 41 | unbounded | 0 |
| 42 | unbounded | 0 |
| 43 | unbounded | 0 |
| 44 | unbounded | 0 |
| 45 | unbounded | 0 |
| 46 | unbounded | 0 |
| 47 | unbounded | 0 |
| 48 | unbounded | 0 |
| 49 | unbounded | 0 |
| 50 | unbounded | 0 |
| 51 | unbounded | 0 |
| 52 | unbounded | 0 |
| 53 | unbounded | 0 |
| 54 | unbounded | 0 |
| 55 | unbounded | 0 |
| 56 | unbounded | 0 |
| 57 | unbounded | 0 |
| 58 | unbounded | 0 |
| 59 | unbounded | 0 |
| 0 | bounded (200) | 100 |
| 1 | bounded (200) | 100 |
| 2 | bounded (200) | 100 |
| 3 | bounded (200) | 100 |
| 4 | bounded (200) | 100 |
| 5 | bounded (200) | 100 |
| 6 | bounded (200) | 100 |
| 7 | bounded (200) | 100 |
| 8 | bounded (200) | 100 |
| 9 | bounded (200) | 100 |
| 10 | bounded (200) | 100 |
| 11 | bounded (200) | 100 |
| 12 | bounded (200) | 100 |
| 13 | bounded (200) | 100 |
| 14 | bounded (200) | 100 |
| 15 | bounded (200) | 100 |
| 16 | bounded (200) | 100 |
| 17 | bounded (200) | 100 |
| 18 | bounded (200) | 100 |
| 19 | bounded (200) | 100 |
| 20 | bounded (200) | 100 |
| 21 | bounded (200) | 100 |
| 22 | bounded (200) | 100 |
| 23 | bounded (200) | 100 |
| 24 | bounded (200) | 100 |
| 25 | bounded (200) | 100 |
| 26 | bounded (200) | 100 |
| 27 | bounded (200) | 100 |
| 28 | bounded (200) | 100 |
| 29 | bounded (200) | 100 |
| 30 | bounded (200) | 100 |
| 31 | bounded (200) | 100 |
| 32 | bounded (200) | 100 |
| 33 | bounded (200) | 100 |
| 34 | bounded (200) | 100 |
| 35 | bounded (200) | 100 |
| 36 | bounded (200) | 100 |
| 37 | bounded (200) | 100 |
| 38 | bounded (200) | 100 |
| 39 | bounded (200) | 100 |
| 40 | bounded (200) | 100 |
| 41 | bounded (200) | 100 |
| 42 | bounded (200) | 100 |
| 43 | bounded (200) | 100 |
| 44 | bounded (200) | 100 |
| 45 | bounded (200) | 100 |
| 46 | bounded (200) | 100 |
| 47 | bounded (200) | 100 |
| 48 | bounded (200) | 100 |
| 49 | bounded (200) | 100 |
| 50 | bounded (200) | 100 |
| 51 | bounded (200) | 100 |
| 52 | bounded (200) | 100 |
| 53 | bounded (200) | 100 |
| 54 | bounded (200) | 100 |
| 55 | bounded (200) | 100 |
| 56 | bounded (200) | 100 |
| 57 | bounded (200) | 100 |
| 58 | bounded (200) | 100 |
| 59 | bounded (200) | 100 |
| 0 | bounded (200) + dequeue check | 100 |
| 1 | bounded (200) + dequeue check | 100 |
| 2 | bounded (200) + dequeue check | 100 |
| 3 | bounded (200) + dequeue check | 100 |
| 4 | bounded (200) + dequeue check | 100 |
| 5 | bounded (200) + dequeue check | 100 |
| 6 | bounded (200) + dequeue check | 100 |
| 7 | bounded (200) + dequeue check | 100 |
| 8 | bounded (200) + dequeue check | 100 |
| 9 | bounded (200) + dequeue check | 100 |
| 10 | bounded (200) + dequeue check | 100 |
| 11 | bounded (200) + dequeue check | 100 |
| 12 | bounded (200) + dequeue check | 100 |
| 13 | bounded (200) + dequeue check | 100 |
| 14 | bounded (200) + dequeue check | 100 |
| 15 | bounded (200) + dequeue check | 100 |
| 16 | bounded (200) + dequeue check | 100 |
| 17 | bounded (200) + dequeue check | 100 |
| 18 | bounded (200) + dequeue check | 100 |
| 19 | bounded (200) + dequeue check | 100 |
| 20 | bounded (200) + dequeue check | 100 |
| 21 | bounded (200) + dequeue check | 100 |
| 22 | bounded (200) + dequeue check | 100 |
| 23 | bounded (200) + dequeue check | 100 |
| 24 | bounded (200) + dequeue check | 100 |
| 25 | bounded (200) + dequeue check | 100 |
| 26 | bounded (200) + dequeue check | 100 |
| 27 | bounded (200) + dequeue check | 100 |
| 28 | bounded (200) + dequeue check | 100 |
| 29 | bounded (200) + dequeue check | 100 |
| 30 | bounded (200) + dequeue check | 100 |
| 31 | bounded (200) + dequeue check | 100 |
| 32 | bounded (200) + dequeue check | 100 |
| 33 | bounded (200) + dequeue check | 100 |
| 34 | bounded (200) + dequeue check | 100 |
| 35 | bounded (200) + dequeue check | 100 |
| 36 | bounded (200) + dequeue check | 100 |
| 37 | bounded (200) + dequeue check | 100 |
| 38 | bounded (200) + dequeue check | 100 |
| 39 | bounded (200) + dequeue check | 100 |
| 40 | bounded (200) + dequeue check | 100 |
| 41 | bounded (200) + dequeue check | 100 |
| 42 | bounded (200) + dequeue check | 100 |
| 43 | bounded (200) + dequeue check | 100 |
| 44 | bounded (200) + dequeue check | 100 |
| 45 | bounded (200) + dequeue check | 100 |
| 46 | bounded (200) + dequeue check | 100 |
| 47 | bounded (200) + dequeue check | 100 |
| 48 | bounded (200) + dequeue check | 100 |
| 49 | bounded (200) + dequeue check | 100 |
| 50 | bounded (200) + dequeue check | 100 |
| 51 | bounded (200) + dequeue check | 100 |
| 52 | bounded (200) + dequeue check | 100 |
| 53 | bounded (200) + dequeue check | 100 |
| 54 | bounded (200) + dequeue check | 100 |
| 55 | bounded (200) + dequeue check | 100 |
| 56 | bounded (200) + dequeue check | 100 |
| 57 | bounded (200) + dequeue check | 100 |
| 58 | bounded (200) + dequeue check | 100 |
| 59 | bounded (200) + dequeue check | 100 |
Goodput
| Queue | Over the minute (/s) | Last 10 s (/s) |
|---|---|---|
| unbounded | 12.5 | 0 |
| bounded (200) | 100 | 100 |
| bounded (200) + dequeue check | 100 | 100 |
unbounded goodput 12.5/s over the minute, 0.0/s in the last 10 s
bounded (200) goodput 100.0/s over the minute, 100.0/s in the last 10 s
bounded (200) + dequeue check goodput 100.0/s over the minute, 100.0/s in the last 10 sWhat we found
The unbounded queue delivered 12.5 answers per second over the minute and 0 in the last ten seconds: after the first few seconds every request had expired by the time it was served. The bounded queue delivered 100 per second, and so did the bounded queue with the deadline check: a bound of 200 at 100 per second means no request waits more than 2 seconds, so with this bound the check never had anything to discard.
Read it
The bound is what turns overload into rejected requests instead of expired ones; it should be set from the timeout (capacity × timeout), not from memory. The deadline check is the safety net for when the bound is larger than that, or when clients' timeouts vary.