Skip to content
About
Main content

Availability of unanimity: 0.999ⁿ versus a majority

Simulate a modelNeeds Python with NumPy

Problem

across n participants needs every one of them up to decide; needs only a majority. With each node available with probability a, unanimity is available a^n of the time, which falls with every participant, while a majority of a small odd group is available far more than any single node. Correlated failures (a rack, a zone) change the picture: they are rarer than independent ones but take out several nodes at once.

Compute for unanimity and majority for n = 1, 3, 5, 10, 20 with a = 0.999. Then check it by random simulation: draw each node up with probability 0.999 for 10 million rounds, count rounds in which all 10 are up and rounds in which at least 3 of 5 are up. Then add correlation: with probability 0.0005 per round, a rack failure takes out several nodes at once.

The question. How available are unanimity and majority, and what do correlated failures change?


Predict

With correlated rack failures added, what happens to the 5-node majority's availability?

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-of-unanimity-0-999-versus.py47 linesDownload
    View the code
    availability-of-unanimity-0-999-versus.py
    # /// script
    # requires-python = ">=3.10"
    # dependencies = ["numpy"]
    # ///
    # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT
    # https://thedataengineering.com/lab-code/LICENSE.txt
    """Availability of unanimity against a majority.
    
    Atomic commit needs every participant up; consensus needs a majority. With
    each node up with probability 0.999: a^n against the binomial majority, for n
    from 1 to 20; then 10 million simulated rounds for 10-node unanimity and a
    3-of-5 majority, independent and with a correlated rack failure (probability
    0.0005 per round of losing 5 nodes at once).
    """
    import argparse, json
    from math import comb
    import numpy as np
    
    p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    p.add_argument("--a", type=float, default=0.999, help="Node availability.")
    p.add_argument("--rounds", type=int, default=10_000_000, help="Simulated rounds.")
    p.add_argument("--rack-p", type=float, default=0.0005, help="Per-round probability that a rack of 5 fails together.")
    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()
    
    a = args.a
    def unanimity(n): return a ** n
    def majority(n): return sum(comb(n, k) * a ** k * (1 - a) ** (n - k) for k in range(n // 2 + 1, n + 1))
    formula = [{"n": n, "unanimity": unanimity(n), "majority": majority(n)} for n in (1, 3, 5, 10, 20)]
    rng = np.random.default_rng(args.seed); sims = []
    for correlated in (False, True):
        up10 = np.ones(args.rounds, dtype=np.int8); up5 = np.zeros(args.rounds, dtype=np.int16)
        for i in range(10): up10 &= (rng.random(args.rounds) < a)
        rack = rng.random(args.rounds) < args.rack_p if correlated else np.zeros(args.rounds, dtype=bool)
        all10 = up10.astype(bool) & ~rack                                        # the 10 participants span the failed rack
        for i in range(5): up5 += (rng.random(args.rounds) < a)
        maj = (up5 >= 3) & ~(rack & (rng.random(args.rounds) < 0.6))            # a rack failure takes 3 of the 5 replicas 60% of the time
        sims.append({"failures": "correlated rack failures" if correlated else "independent", "unanimity_10": float(all10.mean()),
                     "majority_3_of_5": float(maj.mean())})
    for r in formula: print(f"n = {r['n']:>2}: all up {r['unanimity']:.6f}   majority up {r['majority']:.10f}")
    for r in sims: print(f"simulated, {r['failures']:<26} 10 all up {r['unanimity_10']:.5f}   3 of 5 up {r['majority_3_of_5']:.8f}")
    
    if args.json:
        with open(args.json, "w") as f:
            json.dump({"tables": {"formula": formula, "simulated": sims}}, 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-of-unanimity-0-999-versus.py
    uv run availability-of-unanimity-0-999-versus.py --help     # every knob
Knobs
Knob · defaultWhat it changes
--a0.999 Node availability.
--rounds10,000,000 Simulated rounds.
--rack-p0.0005 Per-round probability that a rack of 5 fails together.
--seed0 Random seed.

Our run

Independent failures, a = 0.999

Independent failures, a = 0.999
nAll upMajority up
10.9990.999
30.9971
50.9951
100.991
200.981

10 million simulated rounds

10 million simulated rounds
Failures10 all up3 of 5 up
independent0.991
correlated rack failures0.991
Reference runApple M2 · 8 cores · 8 GB
Output · uv run availability-of-unanimity-0-999-versus.py
n =  1: all up 0.999000   majority up 0.9990000000
n =  3: all up 0.997003   majority up 0.9999970020
n =  5: all up 0.995010   majority up 0.9999999900
n = 10: all up 0.990045   majority up 1.0000000000
n = 20: all up 0.980189   majority up 1.0000000000
simulated, independent                10 all up 0.99008   3 of 5 up 1.00000000
simulated, correlated rack failures   10 all up 0.98957   3 of 5 up 0.99970680
macOS 26.6.2 · Python 3.13.3 · numpy 2.5.4 · run 2026-10-20

What we found

Unanimity over 10 nodes was available 0.99 of the time, and a 5-node majority 1. The simulation agreed: 0.99 and 1. With rack failures, unanimity barely moved (0.99), while the majority fell to 1: correlation is what breaks a majority, because it takes out several replicas at once.


Read it

Unanimity is fragile by arithmetic and majority is robust only to independent failures. Place replicas so that one correlated event cannot take a majority (across racks and zones), and keep the number of participants in an atomic commit as small as possible.