Skip to content
About
Main content

Conflict grows with the square of concurrency (uniform versus skewed keys)

Simulate a modelNeeds Python with NumPy

Problem

Two 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 , 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

At L = 500, with Zipf-skewed keys instead of uniform ones, how many conflicting pairs are there? (The uniform formula gives 12.5.)

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.

    conflict-grows-with-the-square-of.py42 linesDownload
    View 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)
    
  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 conflict-grows-with-the-square-of.py
    uv run conflict-grows-with-the-square-of.py --help     # every knob
Knobs
Knob · defaultWhat 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

Conflicting pairs against concurrencyApple M2 · 8 cores · 8 GB
UniformL²k²/2NZipf
Data table
Conflicting pairs against concurrency
Concurrent transactions LUniformL²k²/2NZipf
500.140.1251,120
1000.5550.54,520
2002.08218.1K
50012.412.5114K
macOS 26.6.2 · Python 3.13.3 · numpy 2.5.4 · run 2026-10-20
Output · uv run conflict-grows-with-the-square-of.py
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 pairs

What 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.