Skip to content
About
Main content

A shorter timeout makes a failure detector wrong more often

Simulate a modelNeeds Python

Problem

Across a network, a dead node and a slow one look the same: both stop answering. A can only wait, and a is a guess. Wait too briefly and healthy but slow nodes are declared dead, which triggers elections and failovers for nothing; wait too long and a real crash goes unnoticed. Because network delays have fat , any fixed timeout is either slow or wrong. What a timeout can never do, in a correct protocol, is make a decision wrong: comes from counting members and , from timeouts.

Measure the false-suspicion rate of a detector against its timeout (The arithmetic worth carrying, Failure detection: liveness is a relation, not an attribute).

Simulate a node that sends a heartbeat every 100 ms. The network delay is random: 95% of the time 1-20 ms, 5% of the time 100-700 ms (a stand-in for pauses and congestion). The monitor suspects the node when no heartbeat arrives within the timeout.

The script tries timeouts from 150 ms to 10 s, and for each also crashes the node at a random moment to time the detection.

The question. How often does the detector wrongly suspect a healthy node at each timeout, and how long does it take to notice a real crash?


Predict

At a 500 ms timeout, what share of heartbeats triggers a false suspicion?

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.

    a-shorter-timeout-makes-a-failure.py57 linesDownload
    View the code
    a-shorter-timeout-makes-a-failure.py
    # /// script
    # requires-python = ">=3.10"
    # ///
    # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT
    # https://thedataengineering.com/lab-code/LICENSE.txt
    """A shorter timeout makes a failure detector wrong more often.
    
    A node heartbeats every 100 ms over a network that is usually fast and
    sometimes very slow. The monitor suspects the node whenever no heartbeat has
    arrived within the timeout. Measured: false suspicions per heartbeat for a
    range of timeouts, and the time to detect a real crash.
    """
    import argparse, json, random
    
    p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    p.add_argument("--interval-ms", type=float, default=100, help="Heartbeat interval.")
    p.add_argument("--slow-share", type=float, default=0.05, help="Share of heartbeats delayed 100-700 ms (the rest take 1-20 ms).")
    p.add_argument("--timeouts", type=lambda s: [float(v) for v in s.split(",")], default=[150, 200, 300, 500, 700, 1000, 3000, 10000],
                   help="Timeouts to try, in ms, comma-separated.")
    p.add_argument("--beats", type=int, default=100_000, help="Heartbeats per timeout.")
    p.add_argument("--crashes", type=int, default=1000, help="Simulated crashes per timeout, for the detection time.")
    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)
    
    def delay():
        return random.uniform(1, 20) if random.random() >= args.slow_share else random.uniform(100, 700)
    
    def false_suspicions(timeout):
        last_arrival, count = 0.0, 0
        for i in range(1, args.beats + 1):
            arrival = i * args.interval_ms + delay()
            if arrival - last_arrival > timeout:
                count += 1
            last_arrival = max(last_arrival, arrival)
        return count / args.beats
    
    def detection_time(timeout):
        """The node crashes at a random moment; the monitor notices `timeout` after the last heartbeat arrived."""
        total = 0.0
        for _ in range(args.crashes):
            crash = random.uniform(0, args.interval_ms)          # time since the last heartbeat was sent
            last_arrival = delay()                                # that heartbeat's arrival, relative to its send
            total += max(last_arrival + timeout - crash, 0)
        return total / args.crashes
    
    rows = []
    for t in args.timeouts:
        fs, det = false_suspicions(t), detection_time(t)
        rows.append({"timeout_ms": t, "false_suspicion_pct": fs * 100, "detect_ms": det})
        print(f"timeout {t:>6.0f} ms: false suspicions {fs:8.3%} of heartbeats   real crash detected after {det:7.0f} ms")
    
    if args.json:
        with open(args.json, "w") as f:
            json.dump({"tables": {"timeouts": rows}}, 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 a-shorter-timeout-makes-a-failure.py
    uv run a-shorter-timeout-makes-a-failure.py --help     # every knob
Knobs
Knob · defaultWhat it changes
--interval-ms100 Heartbeat interval.
--slow-share0.05 Share of heartbeats delayed 100-700 ms (the rest take 1-20 ms).
--timeouts150, 200, 300, 500, 700, 1000, 3000, 10,000 Timeouts to try, in ms, comma-separated.
--beats100,000 Heartbeats per timeout.
--crashes1000 Simulated crashes per timeout, for the detection time.
--seed0 Random seed.

Our run

False suspicions by timeoutApple M2 · 8 cores · 8 GB
Data table
False suspicions by timeout
Timeout (ms)Heartbeats that trigger a false suspicion
1504.62%
2004.55%
3003.65%
5002.05%
7000.603%
1,0000%
3,0000%
10K0%
macOS 26.6.2 · Python 3.13.3 · run 2026-10-20
Time to detect a real crashApple M2 · 8 cores · 8 GB
Data table
Time to detect a real crash
Timeout (ms)Detection time (ms)
150131
200177
300282
500475
700684
1,000980
3,0002,980
10K9,980
macOS 26.6.2 · Python 3.13.3 · run 2026-10-20
Output · uv run a-shorter-timeout-makes-a-failure.py
timeout    150 ms: false suspicions   4.623% of heartbeats   real crash detected after     131 ms
timeout    200 ms: false suspicions   4.553% of heartbeats   real crash detected after     177 ms
timeout    300 ms: false suspicions   3.645% of heartbeats   real crash detected after     282 ms
timeout    500 ms: false suspicions   2.047% of heartbeats   real crash detected after     475 ms
timeout    700 ms: false suspicions   0.603% of heartbeats   real crash detected after     684 ms
timeout   1000 ms: false suspicions   0.000% of heartbeats   real crash detected after     980 ms
timeout   3000 ms: false suspicions   0.000% of heartbeats   real crash detected after    2978 ms
timeout  10000 ms: false suspicions   0.000% of heartbeats   real crash detected after    9977 ms

What we found

At a 150 ms timeout, 4.62% of heartbeats triggered a false suspicion, nearly every slow heartbeat; at 500 ms it was still 2.05%. The rate reached zero only at 1,000 ms (0%), once the timeout exceeded the slowest delay plus the heartbeat interval. Detection of a real crash tracked the timeout closely: 131 ms at a 150 ms timeout, 9,980 ms at 10 s. Every millisecond removed from detection is paid for in false suspicions, as expected.


Read it

You now have both sides of the trade: speed of detection against false positives. The timeout changes liveness only; no value of it can make a decision wrong if the safety rule uses epochs.

The false-suspicion rate is the probability that a delay exceeds the timeout, the tail of the distribution, which tuning cannot shrink. Shortening the timeout to fail over faster (500 ms instead of 10 s) turns every GC pause and congested moment into a spurious election: the system does not become incorrect, it becomes unavailable. Adaptive detectors such as phi-accrual do not escape the trade; they let each caller choose its own point on it.