Gateway Graceful Drain
Let in-flight bot turns finish before the gateway process exits — opt-in with a single timeout setting
When a gateway process receives SIGTERM or SIGINT, it can wait for in-flight bot turns to complete before exiting — instead of cutting them off mid-response. Configure the drain window via the CLI, YAML, or Python.
from praisonaiagents import Agent
from praisonai.bots.botos import BotOS
agent = Agent(
name="SupportBot",
instructions="Help users with support questions.",
model="gpt-4o-mini",
)
bots = BotOS(
agent=agent,
drain_timeout=30,
)
bots.run()The user sends SIGTERM to the gateway process; graceful drain stops new work and waits for in-flight bot turns up to drain_timeout.
Quick Start
- Enable via the CLI flag
bash praisonai gateway start gateway.yaml --drain-timeout 30The gateway waits up to 30 seconds for in-flight turns to complete before exiting.
- Enable via YAML config
yaml # gateway.yaml drain_timeout: 30 agent: name: assistant instructions: "Help users" model: gpt-4o-mini channels: telegram: platform: telegram token: "${TELEGRAM_BOT_TOKEN}"bash praisonai gateway start gateway.yaml - Enable via Python
python from praisonaiagents import Agent from praisonai.bots.botos import BotOS agent = Agent( name="Bot", instructions="Be helpful.", model="gpt-4o-mini", ) bots = BotOS(agent=agent, drain_timeout=30) bots.run() - Override drain timeout at stop time
You can also pass a one-off timeout when stopping programmatically:
python import asyncio async def main(): bots = BotOS(agent=agent, drain_timeout=30) asyncio.create_task(bots.run_async()) await asyncio.sleep(60) await bots.stop(drain_timeout=10) - Enable via GatewayConfig (Python-first)
Set
drain_timeoutonGatewayConfigdirectly — no CLI flag and no YAML key required. This closes the CLI + YAML + Python parity gap for embedders wiring up a gateway from Python.python from praisonaiagents.gateway import GatewayConfig config = GatewayConfig(drain_timeout=20) # feed this config into the gateway/BotOS as usualdrain_timeoutmust be>= 0(ValueErrorotherwise);0/Nonedisables the drain window. - Bound a wedged teardown with a diagnostic deadline
stop()also accepts agracebudget — extra seconds beyonddrain_timeoutbefore the shutdown-phase deadline watchdog fires a diagnosable self-exit if teardown itself is wedged (adapterdisconnect(), WS drain, ledger/DB close). Requires the loop watchdog to be configured; a no-op otherwise.python await bots.stop(drain_timeout=30, grace=10) # → wedged teardown self-exits with an all-thread stack dump at ~40s, # instead of hanging until the supervisor's SIGKILL.Full mechanism and YAML config on the Loop Watchdog → Shutdown-phase deadline mode page.
How It Works
The drain phase uses DrainTimeoutPolicy from praisonaiagents.gateway.protocols. If the timeout elapses before all turns finish, the process exits forcefully but still attempts to flush any queued outbound messages.
Behaviour Table
| Scenario | Within drain window | After drain timeout |
|---|---|---|
| In-flight turn (started before signal) | Runs to completion | Force-killed |
| New inbound message (after signal) | Rejected / queued for next instance | Rejected |
| Queued outbound message | Flushed | Best-effort |
Configuration Precedence
When multiple sources specify drain_timeout, the most specific wins:
| Source | Precedence |
|---|---|
bots.stop(drain_timeout=N) call | Highest |
BotOS(drain_timeout=N) constructor | High |
--drain-timeout N CLI flag | Medium-high — overrides YAML / config |
gateway.drain_timeout in YAML | Medium |
GatewayConfig(drain_timeout=N) (new) | Low — used only when no CLI flag and no YAML key |
Default (reliability unset) | Lowest — 5 s drain window applied; use reliability="off" for immediate exit |
The GatewayConfig(drain_timeout=…) field is consulted only when there is no CLI flag and no YAML key. The start_with_config fallback checks "drain_timeout" not in gw_cfg before reading the field, so an explicit drain_timeout: null in YAML wins over the field.
When drain_timeout is 0, the gateway exits immediately on SIGTERM without draining. When drain_timeout is None (and no reliability preset is set), a 5-second drain window is applied by the default posture — use reliability="off" to restore immediate-teardown behaviour.
Best Practices
Set drain_timeout in Kubernetes or container deployments
Container orchestrators send SIGTERM before force-killing a pod. Set drain_timeout to slightly less than the terminationGracePeriodSeconds in your pod spec:
# kubernetes deployment
terminationGracePeriodSeconds: 60
# gateway.yaml
drain_timeout: 50This gives the gateway 50 seconds to finish turns and 10 seconds of buffer for the process to exit cleanly.
Keep the timeout proportional to your P99 turn latency
If your agent typically responds in 5 seconds and your P99 is 20 seconds, set drain_timeout: 25. A timeout much larger than your P99 adds unnecessary shutdown delay without benefit.
Cross-link with gateway-scale-to-zero
Graceful drain is especially important for scale-to-zero deployments. Combine it with the existing scale-to-zero feature to ensure scale-down events don’t interrupt active users.
Pair drain_timeout with the shutdown-phase deadline
drain_timeout bounds how long the gateway waits for in-flight turns; it does not bound how long a wedged teardown step (adapter disconnect(), WebSocket drain, ledger/DB close) can hang after the wait window ends. For that, enable the loop watchdog and set gateway.watchdog.shutdown_grace (or pass grace=… to stop()) — a wedged teardown then self-exits with an all-thread stack dump and exit code 75 at drain_timeout + shutdown_grace, instead of sitting silent until systemd’s TimeoutStopSec reaches SIGKILL. Added in PR #5084 / issue #5079. See Loop Watchdog → Shutdown-phase deadline mode.
Related
Scale the gateway down when idle and back up on demand
Gateway Drain TriggerPort-less external drain signal for hosted deployments
CardDetect a wedged loop and bound a wedged shutdown with a diagnostic self-exit.
CardThe front-rung sibling — bound a wedged pre-loop startup with the same diagnostic self-exit.