Conflict grows with the square of concurrency (uniform versus skewed keys)
Simulate a modelNeeds Python with NumPy
Problem
Two concurrent transactions conflict when they touch a common key. With L transactions alive at once, there are about L²/2 pairs, and each pair shares a key with probability about k²/N when each touches k of N keys uniformly. So conflicts grow with the square of concurrency: doubling the transactions in flight quadruples the conflicts. Skewed access concentrates transactions on a few hot keys, which behaves like a much smaller N.
Simulate L transactions alive at once, each choosing k = 10 random keys out of N = 1,000,000. Count how many pairs share a key. Compare with the formula L²k²/2N, then repeat with a skewed (Zipf) key choice.
The question. Does the number of conflicting pairs follow L²k²/2N, and what does skew do to it?
Predict
Expect the uniform result to match the formula, and the skewed result to be far higher, as if N were two or three orders of magnitude smaller. Doubling L multiplies conflicts by about 4 (not 2). For L = 500 the formula gives 12.5.
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
conflict-grows-with-the-square-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 """Conflict grows with the square of concurrency. L transactions alive at once, each touching k random keys out of N: the number of pairs that share a key, against L²k²/2N, for uniform and Zipf-skewed key choice. """ import argparse, json import numpy as np p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) p.add_argument("--n", type=int, default=1_000_000, help="Keys N.") p.add_argument("--k", type=int, default=10, help="Keys per transaction k.") p.add_argument("--ls", default="50,100,200,500", help="Concurrent transactions L.") p.add_argument("--zipf", type=float, default=1.2, help="Zipf exponent for the skewed run.") p.add_argument("--trials", type=int, default=200, help="Trials per point.") p.add_argument("--json", help="Also write the results to this JSON file.") args = p.parse_args() def conflicting_pairs(L, zipf_a=None): rng = np.random.default_rng(1); total = 0 for _ in range(args.trials): keys = [set(rng.integers(0, args.n, args.k)) if zipf_a is None else set((rng.zipf(zipf_a, args.k) - 1) % args.n) for _ in range(L)] for i in range(L): for j in range(i + 1, L): if keys[i] & keys[j]: total += 1 return total / args.trials rows = [] for L in map(int, args.ls.split(",")): rows.append({"L": L, "uniform": conflicting_pairs(L), "formula": L * L * args.k * args.k / (2 * args.n), "zipf": conflicting_pairs(L, args.zipf)}) for i, r in enumerate(rows): r["uniform_vs_prev"] = r["uniform"] / rows[i - 1]["uniform"] if i and rows[i - 1]["uniform"] else None for r in rows: print(f"L = {r['L']:>3}: uniform {r['uniform']:8.2f} pairs (formula {r['formula']:6.2f}) Zipf {r['zipf']:9.1f} pairs") if args.json: with open(args.json, "w") as f: json.dump({"tables": {"concurrency": 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 conflict-grows-with-the-square-of.py uv run conflict-grows-with-the-square-of.py --help # every knob
| Knob · default | What it changes |
|---|---|
| --n1,000,000 | Keys N. |
| --k10 | Keys per transaction k. |
| --ls50, 100, 200, 500 | Concurrent transactions L. |
| --zipf1.2 | Zipf exponent for the skewed run. |
| --trials200 | Trials per point. |
Our run
Data table
| Concurrent transactions L | Uniform | L²k²/2N | Zipf |
|---|---|---|---|
| 50 | 0.14 | 0.125 | 1,120 |
| 100 | 0.555 | 0.5 | 4,520 |
| 200 | 2.08 | 2 | 18.1K |
| 500 | 12.4 | 12.5 | 114K |
L = 50: uniform 0.14 pairs (formula 0.12) Zipf 1119.4 pairs
L = 100: uniform 0.56 pairs (formula 0.50) Zipf 4515.5 pairs
L = 200: uniform 2.08 pairs (formula 2.00) Zipf 18141.8 pairs
L = 500: uniform 12.41 pairs (formula 12.50) Zipf 113578.4 pairsWhat we found
Uniform access followed the formula: 12.4 conflicting pairs at L = 500 against 12.5, and each doubling of L multiplied conflicts by about four (0.555 at 100, 2.08 at 200). Zipf access gave 114K pairs at L = 500, thousands of times the uniform count.
Read it
Concurrency is not free even with no shared hot spot: conflicts grow quadratically, so a system that runs fine at one level of concurrency can collapse into retries at twice that. Skew makes it far worse, and the fix is in the data model (spread hot keys, aggregate them differently) more than in the concurrency control.