Skip to main content
LoopWatchdog detects a wedged asyncio loop and hands the process back to your supervisor before the gateway becomes a silent zombie.
Running praisonai gateway start? You don’t need to write any Python — the gateway wires the watchdog for you. Add gateway.watchdog: { enabled: true } to gateway.yaml, or pass --watchdog on the CLI. See Using it from the gateway below.
Looking for connection heartbeats? Gateway Liveness reaps silent client connections with PING/PONG frames. This page covers the event loop itself — when the process is up but the loop stops running. The two solve orthogonal problems:

Using it from the gateway

Most operators run praisonai gateway start — turn the watchdog on with YAML or a CLI flag, no Python required.

Quick Start (custom runtime)

1

Simple usage (defaults)

Build an agent, then arm the watchdog on the loop that serves it.
2

Tune the cadence

Pass a LoopWatchdogPolicy to change how fast a wedge is caught.
3

Observe without exiting (dev / staging)

Use dump_only to catch a hang without killing the process.

How It Works

A dedicated OS thread probes the loop, so it keeps working precisely when the loop does not. The watchdog uses os._exit, not sys.exit, on a confirmed wedge. Normal interpreter shutdown runs Py_FinalizeEx, which would itself hang trying to join the wedged loop thread — so os._exit hands the process straight back to the supervisor, which restarts it cleanly.

Health signal

When the watchdog is configured, GET /health (or gateway.health() in-process) includes a watchdog block. The block is omitted entirely from health() when the watchdog isn’t configured — an always-on gateway sees no shape change.

Configuration Options

LoopWatchdogPolicy holds every knob. Computed property: LoopWatchdog exposes a small surface:

YAML config keys

The gateway.watchdog block is a thin façade over LoopWatchdogPolicy for gateway operators. An invalid value (for example liveness_interval: 0) fails closed: the gateway logs a warning and runs without a watchdog.

CLI flag reference

  • --watchdog — enable the watchdog for this run (sets/overrides gateway.watchdog.enabled: true).
  • --watchdog-timeout <seconds> — total wedge budget in seconds. Derives liveness_interval = budget / liveness_strikes (keeps the default 3 strikes unless YAML overrides). --watchdog-timeout 15 → probe every 5s. --watchdog-timeout 6 → probe every 2s.

Precedence

Highest wins: --watchdog[-timeout] CLI flag → gateway.watchdog.* YAML block → off. Missing values fall back down the chain — an unset CLI flag does not clobber enabled: true in YAML.

Choosing a cadence

Match the probe interval to how fast a wedge must be caught. Shorter intervals bound recovery time but cost a scheduling roundtrip every N seconds; longer intervals reduce noise and false alerts under GC pauses.
A long stop-the-world GC or a big synchronous blocking call inside a coroutine looks like a wedge if it exceeds wedge_after_s. If you legitimately do 20-second sync work on the loop, either raise wedge_after_s above it or — better — push the work off the loop with run_in_executor / asyncio.to_thread.
faulthandler writes one per-thread traceback on a wedge. Read the loop thread first (typically MainThread or the asyncio worker) — that’s where the actual hang is. Other threads may just be blocked waiting on that one.

Fail-open guarantees

A misbehaving watchdog must never kill a healthy gateway.
  • arm() while already armed is a no-op.
  • disarm() when not armed is safe.
  • Any exception inside probe scheduling, dumping, or exiting is swallowed.
  • A closed / stopped loop is not a wedge — scheduling onto it is a normal shutdown, so the missed counter resets and the watchdog keeps failing open.
  • A disarm() racing an in-flight wedge suppresses the destructive os._exit — a deliberate shutdown never triggers a supervisor restart.

Common Patterns

Three ways teams wire the watchdog. Long-lived embedder (production): keep the defaults and wire the process to your existing supervisor (systemd / Kubernetes / pm2). EX_TEMPFAIL (75) is the same exit code the rest of the restart-intent protocol uses, so any supervisor that already restarts the gateway handles this too.
Diagnostic-only mode (staging): catch a hang without killing the process, inspect the dump, then flip to dump_and_exit in production.
Custom exit code for a bespoke supervisor: override exit_code if you can’t use 75.

Best Practices

systemd’s Restart=on-failure, launchd’s KeepAlive, Docker’s restart: unless-stopped, and Kubernetes all handle exit code 75 correctly. If your supervisor doesn’t, the watchdog will still dump stacks but the process won’t come back — either fix the supervisor or use on_wedge="dump_only" for diagnostics.
CLI flags survive restart via persisted start-flags, but a colleague reading gateway.yaml learns the watchdog is on. The YAML block is the discoverable configuration surface.
Short-lived CLI runs don’t need the watchdog — the process exits anyway. Arm it around gateways, bots, and other loops that must stay up for hours.
The watchdog is a daemon thread and never blocks shutdown, but disarm() also joins it cleanly and prevents any race with a shutdown-time wedge.
A 100ms interval false-positives under GC or big JSON serialisation. Keep probe_interval_s above your longest expected synchronous block on the loop.
systemd (Restart=on-failure) restarts on 75; a naive nohup does not. See Exit Codes for the full restart-intent contract.

Gateway Liveness

Connection-level heartbeats that reap silent clients (disambiguation).

Exit Codes

The shared EX_TEMPFAIL (75) restart-intent protocol.

Forensics

What to keep on unhealthy exits.

Crash-Loop Guard

Guard against restart storms.

Restart Continuation

Re-drive interrupted turns after the restart.