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 runpraisonai gateway start — turn the watchdog on with YAML or a CLI flag, no Python required.
- YAML (recommended)
- CLI flags
- Python (custom runtime)
Add a The gateway arms the watchdog right before
watchdog block under gateway and start as usual._server.serve() and disarms it in finally and on stop().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
Thegateway.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/overridesgateway.watchdog.enabled: true).--watchdog-timeout <seconds>— total wedge budget in seconds. Derivesliveness_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.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 destructiveos._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.
dump_and_exit in production.
exit_code if you can’t use 75.
Best Practices
Turn it on wherever a supervisor can restart the process
Turn it on wherever a supervisor can restart the process
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.Prefer YAML over CLI flags for permanent deployments
Prefer YAML over CLI flags for permanent deployments
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.Only arm in long-lived processes
Only arm in long-lived processes
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.
Always pair arm(loop) with disarm() in finally
Always pair arm(loop) with disarm() in finally
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.Do not shorten the interval below your longest sync block
Do not shorten the interval below your longest sync block
A 100ms interval false-positives under GC or big JSON serialisation. Keep
probe_interval_s above your longest expected synchronous block on the loop.Make sure your supervisor restarts on EX_TEMPFAIL
Make sure your supervisor restarts on EX_TEMPFAIL
systemd (
Restart=on-failure) restarts on 75; a naive nohup does not. See Exit Codes for the full restart-intent contract.Related
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.

