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.

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.

Diagram

Graceful Drain on Shutdown

Yes

No

Yes

Timeout reached

🛑 SIGTERM / SIGINT

Stop accepting\nnew messages

In-flight turns?

⏳ Wait up to\ndrain_timeout seconds

✅ Clean exit

Finished?

⚠️ Force exit

Quick Start

  1. Enable via the CLI flag
    bash
    praisonai gateway start gateway.yaml --drain-timeout 30

    The gateway waits up to 30 seconds for in-flight turns to complete before exiting.

  2. 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
  3. 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()
  4. 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)
  5. Enable via GatewayConfig (Python-first)

    Set drain_timeout on GatewayConfig directly — 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 usual

    drain_timeout must be >= 0 (ValueError otherwise); 0/None disables the drain window.

  6. Bound a wedged teardown with a diagnostic deadline

    stop() also accepts a grace budget — extra seconds beyond drain_timeout before the shutdown-phase deadline watchdog fires a diagnosable self-exit if teardown itself is wedged (adapter disconnect(), 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

Diagram
UserIn-flight TurnIngress (new messages)Gateway ProcessOperating SystemUserIn-flight TurnIngress (new messages)Gateway ProcessOperating SystemRejects any new incoming requestsSIGTERMStop accepting new messagesAllow to complete (up to drain_timeout)Response deliveredTurn completeClean exit (code 0)

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

ScenarioWithin drain windowAfter drain timeout
In-flight turn (started before signal)Runs to completionForce-killed
New inbound message (after signal)Rejected / queued for next instanceRejected
Queued outbound messageFlushedBest-effort

Configuration Precedence

When multiple sources specify drain_timeout, the most specific wins:

SourcePrecedence
bots.stop(drain_timeout=N) callHighest
BotOS(drain_timeout=N) constructorHigh
--drain-timeout N CLI flagMedium-high — overrides YAML / config
gateway.drain_timeout in YAMLMedium
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:

yaml
# kubernetes deployment
terminationGracePeriodSeconds: 60

# gateway.yaml
drain_timeout: 50

This 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