Fan-out tail and power-of-two-choices (about 45 minutes)
Simulate a modelNeeds Python with NumPy
Problem
Two lessons about choosing servers. First, a request that scatters to N shards and gathers all answers inherits the worst of N latencies: at N = 100, most requests contain one shard's p99 event. Second, routing by load information is only as good as that information. Load reports are always a little old, and if every client sends to the server that looked least loaded in the last snapshot, they all pile onto the same one until the next snapshot. Sampling two servers at random and taking the less loaded one (the power of two choices) is robust to stale information.
Claim to test: at N = 100 and per-shard p99, about 63% of requests contain a p99 event, and two random choices beat greedy choice on stale load information (see Scatter-gather and Service discovery).
Scatter-gather: draw N latencies from a log-normal distribution, take the maximum, repeat 100,000 times. Count how often the maximum exceeds the single-shard p99. Run with N = 1, 10, 40 and 100.
Service discovery and load balancing: simulate 10 clients and 20 servers. Every 100 requests the clients refresh a shared load snapshot. Policy 1 sends each request to the server that was least loaded in the snapshot. Policy 2 samples two servers at random and picks the one with less snapshot load. Policy 3 picks one at random. Measure the maximum server load after 10,000 requests.
The script models load as requests in flight (servers finish requests at 90% utilization) and repeats with snapshots every 10, 500 and 2,000 requests.
The question. How often does a 100-shard request contain a p99 event, and which routing policy keeps the busiest server least loaded as the load snapshot gets staler?
Predict
About 1%, 9.6%, 33% and 63% (1 - 0.99^N). Policy 1 is the worst (all requests of one interval go to the same server), policy 3 is moderate, and policy 2 is the best or close to it. The gap grows when you increase the snapshot interval.
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
fan-out-tail-and-power-of.py # /// script # requires-python = ">=3.10" # dependencies = ["numpy"] # /// # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT # https://thedataengineering.com/lab-code/LICENSE.txt """Fan-out tail and the power of two choices. (1) How often a request that fans out to N shards contains a p99 event. (2) Ten clients route 10,000 requests to 20 servers from a load snapshot refreshed every k requests: to the least-loaded server in the snapshot, to the lesser of two random servers, or to one random server. """ import argparse, json, random import numpy as np p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) p.add_argument("--fanouts", type=lambda s: [int(v) for v in s.split(",")], default=[1, 10, 40, 100], help="Shards per request (N).") p.add_argument("--trials", type=int, default=100_000, help="Requests per N.") p.add_argument("--servers", type=int, default=20, help="Servers.") p.add_argument("--requests", type=int, default=10_000, help="Requests routed.") p.add_argument("--intervals", type=lambda s: [int(v) for v in s.split(",")], default=[10, 100, 500, 2000], help="Requests between snapshot refreshes, comma-separated.") p.add_argument("--utilization", type=float, default=0.9, help="Arrival rate over total service capacity (in-flight load model).") 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() rng = np.random.default_rng(args.seed); random.seed(args.seed) fan = [] p99 = float(np.exp(2.326)) # p99 of a standard log-normal for n in args.fanouts: worst = np.exp(rng.standard_normal((args.trials, n))).max(axis=1) fan.append({"N": n, "measured_pct": float(np.mean(worst > p99) * 100), "formula_pct": (1 - 0.99 ** n) * 100}) print(f"N = {n:>3}: request contains a p99 event {fan[-1]['measured_pct']:5.1f}% (1 - 0.99^N = {fan[-1]['formula_pct']:5.1f}%)") def route(policy, interval): """In-flight load: one request arrives per step; each server finishes one every servers/utilization steps.""" load = [0] * args.servers; snap = load[:]; peak = 0 for i in range(args.requests): if i % interval == 0: snap = load[:] if policy == "least loaded (snapshot)": s = min(range(args.servers), key=lambda j: snap[j]) elif policy == "two random choices": a, b = random.sample(range(args.servers), 2); s = a if snap[a] <= snap[b] else b else: s = random.randrange(args.servers) load[s] += 1 if i % max(1, round(args.servers / args.utilization)) == 0: # every server finishes one in-flight request load = [max(0, x - 1) for x in load] peak = max(peak, max(load)) return peak lb = [] for interval in args.intervals: for policy in ("least loaded (snapshot)", "two random choices", "one random"): lb.append({"interval": interval, "policy": policy, "max_load": route(policy, interval)}) print(f"snapshot every {interval:>5}: " + " ".join(f"{r['policy']} {r['max_load']}" for r in lb[-3:])) if args.json: with open(args.json, "w") as f: json.dump({"tables": {"fanout": fan, "routing": lb}}, 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 fan-out-tail-and-power-of.py uv run fan-out-tail-and-power-of.py --help # every knob
| Knob · default | What it changes |
|---|---|
| --fanouts1, 10, 40, 100 | Shards per request (N). |
| --trials100,000 | Requests per N. |
| --servers20 | Servers. |
| --requests10,000 | Requests routed. |
| --intervals10, 100, 500, 2000 | Requests between snapshot refreshes, comma-separated. |
| --utilization0.9 | Arrival rate over total service capacity (in-flight load model). |
| --seed0 | Random seed. |
Our run
Data table
| Requests between snapshot refreshes | series | Peak load (requests) |
|---|---|---|
| 10 | least loaded (snapshot) | 55 |
| 10 | two random choices | 49 |
| 10 | one random | 107 |
| 100 | least loaded (snapshot) | 132 |
| 100 | two random choices | 53 |
| 100 | one random | 76 |
| 500 | least loaded (snapshot) | 478 |
| 500 | two random choices | 72 |
| 500 | one random | 100 |
| 2K | least loaded (snapshot) | 1,910 |
| 2K | two random choices | 135 |
| 2K | one random | 94 |
Requests that contain a p99 event
| N | Simulated | 1 − 0.99^N |
|---|---|---|
| 1 | 1.02% | 1% |
| 10 | 9.71% | 9.56% |
| 40 | 33.3% | 33.1% |
| 100 | 63.4% | 63.4% |
N = 1: request contains a p99 event 1.0% (1 - 0.99^N = 1.0%)
N = 10: request contains a p99 event 9.7% (1 - 0.99^N = 9.6%)
N = 40: request contains a p99 event 33.3% (1 - 0.99^N = 33.1%)
N = 100: request contains a p99 event 63.4% (1 - 0.99^N = 63.4%)
snapshot every 10: least loaded (snapshot) 55 two random choices 49 one random 107
snapshot every 100: least loaded (snapshot) 132 two random choices 53 one random 76
snapshot every 500: least loaded (snapshot) 478 two random choices 72 one random 100
snapshot every 2000: least loaded (snapshot) 1909 two random choices 135 one random 94What we found
The fan-out tail matched the formula: at N = 100, 63.4% of requests contained a p99 event.
Routing behaved as expected once the snapshot was stale. With a refresh every 100 requests the greedy policy's busiest server peaked at 132 requests, against 53 for two random choices and 76 for one random choice. The gap grew with staleness: at a refresh every 2,000 requests, greedy reached 1,910, nearly every request of the interval on one server. Only with very fresh information (every 10 requests) was greedy competitive (55 against 49).
Read it
Greedy routing on stale data is a herd: every client makes the same "best" choice from the same old picture. Randomness breaks the herd, and comparing just two candidates captures most of the benefit of perfect information; the maximum load of two random choices grows like log log n instead of log n / log log n for one random choice.
The fan-out lesson is the same one as in the fan-out arithmetic lab: past a few dozen shards, the p99 of a single shard is the median of the request, so tail work on the shards pays off at the request level many times over.