Hedging: when it helps and when it does not (about 40 minutes)
Simulate a modelNeeds Python
Problem
A hedged request sends a second copy of a request that has not answered by some deadline (often the p95 latency) and uses whichever copy answers first. It is cheap: only the slow 5% get a second copy. Whether it helps depends on where the slowness lives. If it belongs to the attempt (one busy server, one dropped packet), a second attempt is almost always fast. If it belongs to the request (a huge query, a hot key), the second copy is just as slow, and the hedge only adds load.
Claim to test: hedging at p95 turns a 1-in-20 slow tail into about 1-in-400 if slowness is attempt-local, and does nothing if it is request-local (see Hedging).
Simulate 1,000,000 requests. Each attempt takes 10 ms, except that with probability 0.05 it takes 200 ms.
The question. How much does a hedge at 40 ms shrink the slow tail when slowness is attempt-local, and when it is request-local, and can the hedge rate tell the two apart?
Predict
Plain is about 0.05. Attempt-local hedging gives about 0.0025 (0.05 x 0.05), a 20x reduction, for about 5% extra requests. Request-local hedging stays near 0.05 while the extra load is the same.
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
hedging-when-it-helps-and-when.py # /// script # requires-python = ">=3.10" # /// # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT # https://thedataengineering.com/lab-code/LICENSE.txt """Hedging: when it helps and when it does not. Each attempt takes 10 ms, or 200 ms with probability 0.05. A hedge sends a second copy at 40 ms and takes whichever answers first. If slowness belongs to the attempt, the hedge almost always wins; if it belongs to the request, both copies are slow. """ import argparse, json, random p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) p.add_argument("--requests", type=int, default=1_000_000, help="Simulated requests.") p.add_argument("--slow-share", type=float, default=0.05, help="Probability that an attempt (or a request) is slow.") p.add_argument("--hedge-at", type=float, default=40, help="Milliseconds before the second copy is sent.") p.add_argument("--seed", type=int, default=0, help="Random seed.") p.add_argument("--json", help="Also write the results to this JSON file.") args = p.parse_args() random.seed(args.seed) N, P, H = args.requests, args.slow_share, args.hedge_at def attempt(): return 200 if random.random() < P else 10 def plain(): return attempt() def hedged(): return min(attempt(), H + attempt()) def request_local_hedged(): slow = random.random() < P return min(200 if slow else 10, H + (200 if slow else 10)) rows = [] for name, f in (("no hedge", plain), ("hedge, attempt-local slowness", hedged), ("hedge, request-local slowness", request_local_hedged)): lat = [f() for _ in range(N)] slow = sum(x > 100 for x in lat) / N hedge_rate = sum(x > H for x in lat) / N if "hedge," in name else 0.0 rows.append({"variant": name, "slow_pct": slow * 100, "hedge_rate_pct": hedge_rate * 100}) print(f"{name:<32} slow (>100 ms): {slow:7.3%} hedges sent: {hedge_rate:6.2%}") if args.json: with open(args.json, "w") as f: json.dump({"tables": {"variants": 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 hedging-when-it-helps-and-when.py uv run hedging-when-it-helps-and-when.py --help # every knob
| Knob · default | What it changes |
|---|---|
| --requests1,000,000 | Simulated requests. |
| --slow-share0.05 | Probability that an attempt (or a request) is slow. |
| --hedge-at40 | Milliseconds before the second copy is sent. |
| --seed0 | Random seed. |
Our run
1,000,000 requests
| Variant | Slow (> 100 ms) | Hedges sent |
|---|---|---|
| no hedge | 5.01% | 0% |
| hedge, attempt-local slowness | 0.248% | 5.02% |
| hedge, request-local slowness | 4.97% | 4.97% |
no hedge slow (>100 ms): 5.012% hedges sent: 0.00%
hedge, attempt-local slowness slow (>100 ms): 0.248% hedges sent: 5.02%
hedge, request-local slowness slow (>100 ms): 4.972% hedges sent: 4.97%What we found
Without hedging, 5.01% of requests were slow. With attempt-local slowness the hedge cut that to 0.248%, the expected 20× reduction, for 5.02% extra requests. With request-local slowness it stayed at 4.97%, while sending just as many hedges (4.97%).
Read it
Print the hedge rate in both cases (fraction of requests with latency above 40 ms). It is about 5% either way, which shows that the hedge rate alone cannot tell the two cases apart. To diagnose the difference you must check whether the hedged attempt was also slow.
Hedging is a bet that slowness is independent between attempts. Where it is not (expensive requests, overloaded clusters, hot keys) hedges add load at exactly the moment the system has none to spare, so a hedging client should cap its hedge rate and measure whether its hedges win.