Skip to content
About
Main content

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 is at most , 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; 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

With 30 hard dependencies, each 99.9% available, what availability does the service measure?

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.

    availability-ceiling-and-the-benefit-of.py53 linesDownload
    View 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)
    
  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 availability-ceiling-and-the-benefit-of.py
    uv run availability-ceiling-and-the-benefit-of.py --help     # every knob
Knobs
Knob · defaultWhat 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

a^N
CaseValue
0.999^300.97
0.999^1000.905
0.9999^300.997

Tenants hit by a poison tenant

Tenants hit by a poison tenant
DesignFully hitPartly hit
no cells (one shared fleet)100%0%
10 cells10%0%
shuffle sharding, 100 nodes, 2 per tenant0.02%4.2%
Reference runApple M2 · 8 cores · 8 GB
Output · uv run availability-ceiling-and-the-benefit-of.py
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%
macOS 26.6.2 · Python 3.13.3 · numpy 2.5.4 · run 2026-10-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 of a tenant is its own combination, which almost nobody else shares in full, so the others keep at least one healthy node.