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. Escrow 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
Expect: version 1 sends 1,000 requests plus 1,000 replies; version 2 sends only a handful of transfer messages, and never sells more than 1,000 codes in total.
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
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)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
| Knob · default | What 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
| Version | Messages | Codes sold |
|---|---|---|
| one coordinator | 3,000 | 1,000 |
| escrow | 1,060 | 1,000 |
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)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.