Check that your dead-man's switch really fires
Break it on purposeNeeds Python
Problem
An orchestrator's own alerts run inside the orchestrator. When the scheduler itself stops (crashes, hangs, loses its database), nothing runs, including the checks that would report that nothing is running: the silence looks like health. A dead man's switch inverts the signal: the job sends a heartbeat when it finishes, and a checker outside the orchestrator alerts when heartbeats stop.
The script runs a small scheduler (its own process) that runs a job every 2 seconds; the job's final step sends a heartbeat over HTTP to a checker outside the scheduler, which alerts if no heartbeat arrives within 5 seconds. The scheduler also runs its own SLA check. After 10 seconds the scheduler is stopped (paused, as a hung scheduler would be), and both are watched for 20 seconds.
The question. When the scheduler stops, which alert fires, and how quickly?
Predict
The checker alerts although the orchestrator itself reports nothing wrong. The orchestrator's own SLA alert stays silent while the scheduler is down.
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
check-that-your-dead-mans-switch.py # /// script # requires-python = ">=3.10" # /// # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT # https://thedataengineering.com/lab-code/LICENSE.txt """Check that your dead man's switch works. A scheduler (its own process) runs a job every few seconds; the job's final step sends a heartbeat over HTTP to a checker outside the scheduler, and the scheduler also runs its own SLA check on the job. After a while the scheduler is stopped (SIGSTOP: it hangs, as a stuck scheduler does). The checker alerts when no heartbeat arrives within the deadline; the scheduler's own SLA alert runs inside the scheduler, so it cannot fire. Times both from the stop. """ import argparse, json, os, signal, subprocess, sys, threading, time from http.server import BaseHTTPRequestHandler, HTTPServer p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) p.add_argument("--every", type=float, default=2.0, help="Job interval (s).") p.add_argument("--deadline", type=float, default=5.0, help="Heartbeat deadline (s).") p.add_argument("--stop-after", type=float, default=10.0, help="Seconds before the scheduler is stopped.") p.add_argument("--watch", type=float, default=20.0, help="Seconds to watch after the stop.") p.add_argument("--port", type=int, default=58301, help="Checker port.") p.add_argument("--json", help="Also write the results to this JSON file.") args = p.parse_args() SCHEDULER = r""" import sys, time, urllib.request every, deadline, port = float(sys.argv[1]), float(sys.argv[2]), int(sys.argv[3]) last_ok = time.time() while True: time.sleep(every) time.sleep(0.1) # the job last_ok = time.time() urllib.request.urlopen(f"http://127.0.0.1:{port}/beat").read() # final step: heartbeat if time.time() - last_ok > deadline: # the scheduler's own SLA check print("SLA ALERT", flush=True) """ beats = [] class H(BaseHTTPRequestHandler): def do_GET(self): beats.append(time.time()); self.send_response(200); self.end_headers(); self.wfile.write(b"ok") def log_message(self, *a): pass srv = HTTPServer(("127.0.0.1", args.port), H); threading.Thread(target=srv.serve_forever, daemon=True).start() sched = subprocess.Popen([sys.executable, "-u", "-c", SCHEDULER, str(args.every), str(args.deadline), str(args.port)], stdout=subprocess.PIPE, text=True) sla = []; threading.Thread(target=lambda: [sla.append(time.time()) for line in sched.stdout if "SLA ALERT" in line], daemon=True).start() t0 = time.time(); alert = None; stopped = None while time.time() - t0 < args.stop_after + args.watch: now = time.time() if stopped is None and now - t0 >= args.stop_after: os.kill(sched.pid, signal.SIGSTOP); stopped = now last = beats[-1] if beats else t0 if stopped and alert is None and now - last > args.deadline: alert = now time.sleep(0.05) os.kill(sched.pid, signal.SIGKILL); sched.wait(); srv.shutdown() rows = [{"monitor": "heartbeat checker (outside the scheduler)", "fired": "yes" if alert else "no", "seconds_after_stop": alert - stopped if alert else None}, {"monitor": "scheduler's own SLA alert", "fired": "yes" if any(t > stopped for t in sla) else "no", "seconds_after_stop": None}] summary = {"beats_before_stop": sum(1 for b in beats if b < stopped), "beats_after_stop": sum(1 for b in beats if b >= stopped), "deadline_s": args.deadline} print(f"{summary['beats_before_stop']} heartbeats before the stop, {summary['beats_after_stop']} after") for r in rows: print(f"{r['monitor']:<44} " + (f"alerted {r['seconds_after_stop']:.1f} s after the scheduler stopped" if r["seconds_after_stop"] else f"fired: {r['fired']} in {args.watch:g} s")) if args.json: with open(args.json, "w") as f: json.dump({"summary": summary, "tables": {"monitors": rows}}, 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 check-that-your-dead-mans-switch.py uv run check-that-your-dead-mans-switch.py --help # every knob
| Knob · default | What it changes |
|---|---|
| --every2 | Job interval (s). |
| --deadline5 | Heartbeat deadline (s). |
| --stop-after10 | Seconds before the scheduler is stopped. |
| --watch20 | Seconds to watch after the stop. |
| --port58,301 | Checker port. |
Our run
After the scheduler stopped
| Monitor | Fired | Seconds after the stop |
|---|---|---|
| heartbeat checker (outside the scheduler) | yes | 3.47 |
| scheduler's own SLA alert | no | — |
4 heartbeats before the stop, 0 after
heartbeat checker (outside the scheduler) alerted 3.5 s after the scheduler stopped
scheduler's own SLA alert fired: no in 20 sWhat we found
After 4 heartbeats the scheduler stopped and sent no more. The outside checker alerted 3.47 s after the stop, as soon as the last heartbeat was older than the 5 s deadline. The scheduler's own SLA alert fired: no.
Read it
A monitor must not share fate with what it monitors. Heartbeats from the work, checked by something that runs elsewhere, catch the failures where the system goes quiet, which are the ones its internal alerts cannot report.