Skip to content
About
Main content

Escrow removes coordination

Simulate a modelNeeds Python

Problem

A global limit (1,000 discount codes, a stock level, a budget) looks like it needs a single counter that every sale must ask. That makes every sale a round trip to one coordinator. splits the limit instead: each node owns a share it can spend without asking anyone, and nodes talk only when one runs out and borrows from another. The limit still holds, because the shares never add up to more than the total, but messages are needed only at the boundaries.

Measure the message count of a global budget with and without escrow (Coordination avoidance, which is where this article should end).

Simulate 10 nodes selling 1,000 discount codes. Version 1: every sale asks a single coordinator (count the messages). Version 2: each node starts with 100 codes; when a node runs out it asks the node with the most remaining codes for half of that node's remainder (count the messages). Use random, skewed demand across nodes.

The script sends 1,500 purchase attempts, so the budget runs out and the limit is tested.

The question. How many messages does each version send to sell the same 1,000 codes, and does escrow ever oversell?


Predict

How many messages does the escrow version send to sell the same 1,000 codes?

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.

    escrow-removes-coordination.py54 linesDownload
    View the code
    escrow-removes-coordination.py
    # /// script
    # requires-python = ">=3.10"
    # ///
    # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT
    # https://thedataengineering.com/lab-code/LICENSE.txt
    """Escrow removes coordination.
    
    Ten nodes sell a global budget of 1,000 discount codes under skewed demand.
    Version 1 asks one coordinator for every sale; version 2 gives each node its
    own share and moves half of the richest node's remainder when a node runs out.
    """
    import argparse, json, random
    
    p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    p.add_argument("--nodes", type=int, default=10, help="Selling nodes.")
    p.add_argument("--codes", type=int, default=1000, help="Global budget of codes.")
    p.add_argument("--demand", type=int, default=1500, help="Purchase attempts (more than the budget, so it runs out).")
    p.add_argument("--skew", type=float, default=1.2, help="Zipf exponent of demand across nodes.")
    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)
    weights = [1 / (i + 1) ** args.skew for i in range(args.nodes)]
    attempts = random.choices(range(args.nodes), weights, k=args.demand)
    
    # Version 1: a coordinator holds the count; every sale is a request and a reply
    left, msgs1, sold1 = args.codes, 0, 0
    for _ in attempts:
        msgs1 += 2
        if left > 0: left -= 1; sold1 += 1
    
    # Version 2: escrow — each node holds a share, and only a node that runs out sends messages
    share = [args.codes // args.nodes] * args.nodes
    msgs2, sold2, transfers = 0, 0, 0
    for n in attempts:
        if share[n] == 0:
            donor = max(range(args.nodes), key=lambda i: share[i])
            msgs2 += 2                                        # ask the richest node, it replies with half
            if share[donor] > 0:
                give = (share[donor] + 1) // 2
                share[donor] -= give; share[n] += give; transfers += 1
        if share[n] > 0: share[n] -= 1; sold2 += 1
    
    print(f"{args.demand:,} purchase attempts, skewed across {args.nodes} nodes, {args.codes:,} codes")
    print(f"coordinator: {msgs1:,} messages, {sold1:,} codes sold")
    print(f"escrow:      {msgs2:,} messages ({transfers} transfers), {sold2:,} codes sold (never more than {args.codes:,})")
    
    if args.json:
        with open(args.json, "w") as f:
            json.dump({"tables": {"versions": [
                {"version": "one coordinator", "messages": msgs1, "sold": sold1},
                {"version": "escrow", "messages": msgs2, "sold": sold2}],
                "summary": [{"msgs1": msgs1, "msgs2": msgs2, "transfers": transfers, "sold2": sold2, "codes": args.codes}]}}, 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 escrow-removes-coordination.py
    uv run escrow-removes-coordination.py --help     # every knob
Knobs
Knob · defaultWhat it changes
--nodes10 Selling nodes.
--codes1000 Global budget of codes.
--demand1500 Purchase attempts (more than the budget, so it runs out).
--skew1.2 Zipf exponent of demand across nodes.
--seed0 Random seed.

Our run

Selling 1,000 codes

Selling 1,000 codes
VersionMessagesCodes sold
one coordinator3,0001,000
escrow1,0601,000
Reference runApple M2 · 8 cores · 8 GB
Output · uv run escrow-removes-coordination.py
1,500 purchase attempts, skewed across 10 nodes, 1,000 codes
coordinator: 3,000 messages, 1,000 codes sold
escrow:      1,058 messages (29 transfers), 1,000 codes sold (never more than 1,000)
macOS 26.6.2 · Python 3.13.3 · run 2026-10-20

What we found

The coordinator version sent 3,000 messages: two for every one of the 1,500 attempts, including the ones after the codes ran out. Escrow sent 1,060, more than "a handful": 29 transfers, plus the requests of nodes that had run out once the whole budget was spent, when every attempt found its own share empty and asked a neighbour who had none left either. It sold exactly 1,000 codes, never more than the budget.

Before the budget runs low, escrow is almost silent; the messages concentrate where coordination is actually needed, at the end of the budget.


Read it

Escrow turns a constraint that is not invariant-confluent (an upper bound) into many local constraints that are: each node only has to check its own share. The coordination does not disappear; it moves to the rare moments when shares are rebalanced, and its cost is set by how fast shares run out, not by how many sales there are.

The last stretch of any budget still needs coordination, which shows in the message count once the codes are gone. Real escrow systems reserve a small central pool for that final stretch, or simply refuse sales once every share is empty.