Availability ceiling and the benefit of cells
Simulate a modelNeeds Python
Problem
A service that needs all of N dependencies is up only when all of them are: its availability is at most a^N, so thirty dependencies at three nines cap it near 97%. Two designs push back. A soft dependency has a fallback, so only the failures the fallback cannot cover count. Cells split tenants into isolated copies of the system, so a failure caused by one tenant (a poison request, a runaway workload) takes down one cell instead of everyone; shuffle sharding gives each tenant a random small set of nodes, so two tenants rarely share all of theirs.
Check the arithmetic a^N and the effect of cells with a Monte Carlo run: N = 30 hard dependencies, each up with probability 0.999 per simulated minute, over 1,000,000 minutes; then a fallback that works 95% of the time; then 100 tenants with a poison tenant, without cells, in 10 cells, and with shuffle sharding over n = 100 nodes and k = 2 (4,950 pairs).
The question. Does the measured availability match a^N, what does a fallback buy, and how many tenants does a poison tenant hit in each design?
Predict
0.999^30, 0.999^100 and 0.9999^30 are about 0.970, 0.905 and 0.997. The measured availability should match a^N. A poison tenant should affect about 1/4,950 of tenants fully, plus tenants that share one node partially.
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
availability-ceiling-and-the-benefit-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 """The availability ceiling, and the benefit of cells. a^N for N hard dependencies, checked by Monte Carlo; soft dependencies with a fallback; then a poison tenant against no cells, 10 cells, and shuffle sharding over 100 nodes with 2 nodes per tenant. """ import argparse, itertools, json, random import numpy as np p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) p.add_argument("--deps", type=int, default=30, help="Hard dependencies N.") p.add_argument("--a", type=float, default=0.999, help="Availability of one dependency per minute.") p.add_argument("--minutes", type=int, default=1_000_000, help="Simulated minutes.") p.add_argument("--fallback", type=float, default=0.95, help="Share of failures a soft dependency's fallback covers.") p.add_argument("--tenants", type=int, default=100, help="Tenants.") p.add_argument("--nodes", type=int, default=100, help="Nodes for shuffle sharding.") 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); random.seed(args.seed) formulas = [{"case": f"{a}^{n}", "value": a ** n} for a, n in ((0.999, 30), (0.999, 100), (0.9999, 30))] up = rng.random((args.minutes, args.deps)) < args.a hard = float(np.mean(up.all(axis=1))) covered = up | (rng.random((args.minutes, args.deps)) < args.fallback) soft = float(np.mean(covered.all(axis=1))) print(f"{args.deps} hard dependencies at {args.a}: measured {hard:.4f}, a^N = {args.a ** args.deps:.4f}") print(f"with a fallback covering {args.fallback:.0%} of failures: {soft:.5f}") poison = 0 cells = {t: t % 10 for t in range(args.tenants)} hit_cells = sum(1 for t in range(args.tenants) if cells[t] == cells[poison]) / args.tenants pairs = list(itertools.combinations(range(args.nodes), 2)) assign = {t: random.choice(pairs) for t in range(args.tenants * 50)} # many tenants, each on 2 of 100 nodes bad = set(assign[0]) full = sum(1 for t, pr in assign.items() if set(pr) == bad) / len(assign) partial = sum(1 for t, pr in assign.items() if len(set(pr) & bad) == 1) / len(assign) iso = [{"design": "no cells (one shared fleet)", "fully_hit_pct": 100.0, "partly_hit_pct": 0.0}, {"design": "10 cells", "fully_hit_pct": hit_cells * 100, "partly_hit_pct": 0.0}, {"design": f"shuffle sharding, {args.nodes} nodes, 2 per tenant", "fully_hit_pct": full * 100, "partly_hit_pct": partial * 100}] for r in iso: print(f"poison tenant, {r['design']:<40} fully hit {r['fully_hit_pct']:6.2f}% partly {r['partly_hit_pct']:5.2f}%") if args.json: with open(args.json, "w") as f: json.dump({"tables": {"formulas": formulas, "isolation": iso, "summary": [{"hard": hard, "formula": args.a ** args.deps, "soft": soft, "pairs": len(pairs)}]}}, 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 availability-ceiling-and-the-benefit-of.py uv run availability-ceiling-and-the-benefit-of.py --help # every knob
| Knob · default | What it changes |
|---|---|
| --deps30 | Hard dependencies N. |
| --a0.999 | Availability of one dependency per minute. |
| --minutes1,000,000 | Simulated minutes. |
| --fallback0.95 | Share of failures a soft dependency's fallback covers. |
| --tenants100 | Tenants. |
| --nodes100 | Nodes for shuffle sharding. |
| --seed0 | Random seed. |
Our run
a^N
| Case | Value |
|---|---|
| 0.999^30 | 0.97 |
| 0.999^100 | 0.905 |
| 0.9999^30 | 0.997 |
Tenants hit by a poison tenant
| Design | Fully hit | Partly hit |
|---|---|---|
| no cells (one shared fleet) | 100% | 0% |
| 10 cells | 10% | 0% |
| shuffle sharding, 100 nodes, 2 per tenant | 0.02% | 4.2% |
30 hard dependencies at 0.999: measured 0.9705, a^N = 0.9704
with a fallback covering 95% of failures: 0.99850
poison tenant, no cells (one shared fleet) fully hit 100.00% partly 0.00%
poison tenant, 10 cells fully hit 10.00% partly 0.00%
poison tenant, shuffle sharding, 100 nodes, 2 per tenant fully hit 0.02% partly 4.20%What we found
Thirty hard dependencies measured 0.97 against a^N = 0.97. With a fallback covering 95% of failures, availability rose to 0.998. A poison tenant hit 100% of tenants with one shared fleet, 10% with 10 cells, and with shuffle sharding 0.02% fully (about one in 4.95K) and 4.2% partly, those sharing one of its two nodes.
Read it
The ceiling is arithmetic, so adding dependencies lowers it whatever each team does. The two ways out are different: fallbacks reduce how much each failure counts, and isolation reduces how many tenants one failure reaches. Shuffle sharding is the cheap version of cells: the same number of nodes, but the blast radius of a tenant is its own combination, which almost nobody else shares in full, so the others keep at least one healthy node.