Skip to content
About
Main content

The indivisible key

Simulate a modelNeeds Python with NumPy

Problem

Partitioning by key sends every event of a key to one instance, so a key carrying a large share of the traffic sets a floor on the busiest instance's load that no number of instances can lower: the maximum speedup is 1 ÷ that share. splits the hot key into sub-keys spread over several instances, which works for aggregates that can be combined afterwards (counts, sums) and not, without extra work, for those that cannot (a median).

Simulate hash partitioning with 8, 32 and 128 instances. Give one key 30% of the traffic and spread the rest over 100,000 keys. Compute the load of the busiest instance as a share of the total. Then apply salting (8 sub-keys for the hot key) and recompute. Show that the salted result is valid for count but not for the median without extra work.

The question. Does adding instances lower the busiest instance's share, and what does salting change?


Predict

With 128 instances and one key carrying 30% of the traffic, what is the best possible speedup?

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.

    the-indivisible-key.py52 linesDownload
    View the code
    the-indivisible-key.py
    # /// script
    # requires-python = ">=3.10"
    # dependencies = ["numpy"]
    # ///
    # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT
    # https://thedataengineering.com/lab-code/LICENSE.txt
    """The indivisible key.
    
    Hash partitioning over 8, 32 and 128 instances when one key carries 30% of
    the traffic and the rest is spread over 100,000 keys: the busiest instance's
    share of the total. Then the hot key salted into 8 sub-keys. Finally a count
    and a median over the salted sub-keys, combined without a second pass.
    """
    import argparse, json
    import numpy as np
    
    p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    p.add_argument("--events", type=int, default=2_000_000, help="Events.")
    p.add_argument("--hot-share", type=float, default=0.3, help="Share of traffic on the hot key.")
    p.add_argument("--keys", type=int, default=100_000, help="Other keys.")
    p.add_argument("--salts", type=int, default=8, help="Sub-keys for the hot key.")
    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)
    hot = rng.random(args.events) < args.hot_share
    keys = np.where(hot, 0, rng.integers(1, args.keys + 1, args.events)).astype(np.uint64)
    salt = np.where(hot, rng.integers(0, args.salts, args.events), 0).astype(np.uint64)
    def h(x):
        z = x * np.uint64(0x9E3779B97F4A7C15); z ^= z >> np.uint64(31); z *= np.uint64(0xBF58476D1CE4E5B9); return z ^ (z >> np.uint64(29))
    rows = []
    with np.errstate(over="ignore"):
        for n in (8, 32, 128):
            plain = np.bincount((h(keys) % np.uint64(n)).astype(np.int64), minlength=n)
            salted = np.bincount((h(keys * np.uint64(1000) + salt) % np.uint64(n)).astype(np.int64), minlength=n)
            rows.append({"instances": n, "busiest_pct": plain.max() / args.events * 100, "salted_busiest_pct": salted.max() / args.events * 100,
                         "even_pct": 100 / n, "max_speedup": args.events / plain.max(), "salted_max_speedup": args.events / salted.max()})
    values = rng.lognormal(3, 1, args.events)
    hv, hs = values[hot], salt[hot]
    parts = [hv[hs == s] for s in range(args.salts)]
    cnt_ok = sum(len(x) for x in parts) == len(hv)
    true_med = float(np.median(hv)); med_of_meds = float(np.median([np.median(x) for x in parts]))
    summary = {"count_combines": "yes" if cnt_ok else "no", "true_median": true_med, "median_of_medians": med_of_meds,
               "median_err_pct": (med_of_meds / true_med - 1) * 100, "cap": 1 / args.hot_share}
    for r in rows: print(f"{r['instances']:>3} instances: busiest {r['busiest_pct']:5.1f}% (even {r['even_pct']:.1f}%), max speedup {r['max_speedup']:.1f}x; salted ×{args.salts}: busiest {r['salted_busiest_pct']:5.1f}%")
    print(f"salted count adds up exactly: {summary['count_combines']}; median of the {args.salts} partial medians {med_of_meds:.2f} vs true {true_med:.2f}")
    
    if args.json:
        with open(args.json, "w") as f:
            json.dump({"summary": summary, "tables": {"instances": 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 the-indivisible-key.py
    uv run the-indivisible-key.py --help     # every knob
Knobs
Knob · defaultWhat it changes
--events2,000,000 Events.
--hot-share0.3 Share of traffic on the hot key.
--keys100,000 Other keys.
--salts8 Sub-keys for the hot key.
--seed0 Random seed.

Our run

Busiest instance, share of all traffic

Busiest instance, share of all traffic
InstancesBusiestEven shareMax speedupBusiest, salted
838.8%12.5%2.58×16.4%
3232.2%3.13%3.11×9.69%
12830.6%0.781%3.27×8.05%
Reference runApple M2 · 8 cores · 8 GB
Output · uv run the-indivisible-key.py
  8 instances: busiest  38.8% (even 12.5%), max speedup 2.6x; salted ×8: busiest  16.4%
 32 instances: busiest  32.2% (even 3.1%), max speedup 3.1x; salted ×8: busiest   9.7%
128 instances: busiest  30.6% (even 0.8%), max speedup 3.3x; salted ×8: busiest   8.1%
salted count adds up exactly: yes; median of the 8 partial medians 20.03 vs true 20.05
macOS 26.6.2 · Python 3.13.3 · numpy 2.5.4 · run 2026-10-20

What we found

The busiest instance carried 38.8% of the traffic with 8 instances and 30.6% with 128, never below the hot key's share; the speedup topped out at 3.27×. Salting into 8 sub-keys brought the busiest instance to 16.4% with 8 instances and 8.05% with 128: close to even at 8, still far above the even share at 128, since 8 sub-keys spread the hot key over at most 8 instances. The salted count added up exactly (yes); the median of the partial medians was 20 against a true 20, close here only because the salts split the values evenly at random.


Read it

The number of sub-keys sets how far salting spreads a hot key, so it has to grow with the cluster. And salting moves the problem into the combine step: sums and counts merge exactly, medians and distinct counts need their raw values or a .