Skip to content
About
Main content

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 ), the second copy is just as slow, and the hedge only adds load.

Claim to test: at p95 turns a 1-in-20 slow 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 tell the two apart?


Predict

When slowness belongs to the request, not the attempt, how much does a hedge at 40 ms cut the 5% slow share?

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.

    hedging-when-it-helps-and-when.py43 linesDownload
    View 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)
    
  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 hedging-when-it-helps-and-when.py
    uv run hedging-when-it-helps-and-when.py --help     # every knob
Knobs
Knob · defaultWhat 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

1,000,000 requests
VariantSlow (> 100 ms)Hedges sent
no hedge5.01%0%
hedge, attempt-local slowness0.248%5.02%
hedge, request-local slowness4.97%4.97%
Reference runApple M2 · 8 cores · 8 GB
Output · uv run hedging-when-it-helps-and-when.py
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%
macOS 26.6.2 · Python 3.13.3 · run 2026-10-20

What we found

Without hedging, 5.01% of requests were slow. With the hedge cut that to 0.248%, the expected 20× reduction, for 5.02% extra requests. With 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.