stop_grace_period: el agente termina el webhook antes del SIGKILL
Resumen
Cómo no cortar un tool call a los 10s: stop_grace_period en Compose, docker stop -t, distinto del handler SIGTERM. Default Linux 10s / Windows 30s. Fly 5s max 300s. Railway 0s de drain. Techos oficiales curl 3 de septiembre de 2026.

Qué resuelve
Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.
compose stop manda SIGTERM y, por defecto 10s (spec Compose), SIGKILL. Un agente con tool de 20s muere a media respuesta. stop_grace_period: 30s (o lo que mida tu handler). Distinto de graceful shutdown: esa guía es el código; esta es el techo de Compose. Fuentes oficiales consultadas el 3 de septiembre de 2026.
La regla
stop_grace_period: 30s
en el servicio agent. Postgres puede 10s. Caddy 10s. El bot no.
El spec (curl 2026-09-03): stop_grace_period es cuánto espera Compose si el contenedor no maneja SIGTERM (o el stop_signal que hayas puesto) antes de SIGKILL. Duración: ejemplos oficiales 1s y 1m30s. Default: 10 segundos.
docker stop -t 30 es el mismo knob fuera de Compose. Fly/Railway tienen su propio drain; no copies yaml ahí.
Relación con SIGTERM
Sin handler, 30s de espera = 30s idle + KILL. El código debe cerrar el server HTTP y no aceptar webhooks nuevos. init/tini reenvía señales; sin PID 1 correcto el grace no llega.
restart no alarga el stop.
Techos oficiales (CLI vs yaml)
docker container stop (docs CLI, curl 2026-09-03):
- El proceso principal recibe SIGTERM; tras el grace, SIGKILL.
- La primera señal se cambia con
STOPSIGNALen el Dockerfile o--stop-signalenrun/create. -t/--timeout: segundos a esperar. Si no sale, SIGKILL.--timeout -1: sin timeout; el daemon espera indefinido. No lo pongas en CI.- Default si no hay
--stop-timeoutal crear: el daemon usa 10 s Linux y 30 s Windows.
Compose spec stop_signal: si no lo pones, Compose manda SIGTERM. Ejemplo oficial: SIGUSR1. No inventes SIGUSR2 “porque Node”.
docker compose stop y docker compose down exponen -t / --timeout (“shutdown timeout in seconds”). El flag de CLI pisa el yaml. Scripts con compose down --timeout 2 matan el bot aunque el yaml diga 30s.
compose down (docs): para contenedores, redes del bloque networks y la default. Redes/volumes external no se borran. Eso no cambia el grace; sí cambia qué desaparece cuando el timeout gana.
Fly / Railway / serverless
Fly fly.toml (curl 2026-09-03):
- Señal por defecto al apagar una Machine: SIGINT, no SIGTERM. En Node, SIGINT suele ser “Ctrl+C duro”. Pon
kill_signal = "SIGTERM"si tu handler es el de TERM. kill_timeout: espera tras la señal. Default 5 s. Máximo 300 s (5 min). Docs: best-effort; el app debe tolerar cortes más cortos.- Orden: señal al proceso de
ENTRYPOINT/CMD→ esperakill_timeout→ shutdown forzado.
Railway (reference/deployments, curl 2026-09-03): cuando el deploy nuevo está online, el viejo recibe SIGTERM. Default: 0 segundos de drain y luego SIGKILL. No mandan otras señales. El margen es RAILWAY_DEPLOYMENT_DRAINING_SECONDS. 0s + tool de 20s = corte seguro. No copies stop_grace_period al service de Railway.
Workers: isolates; no hay SIGTERM de contenedor. Vercel Functions: maxDuration, no Compose.
Tabla

| Servicio / knob | Grace | Por qué | Techo verificado |
|---|---|---|---|
| agent (Compose) | 30–60s | tools + webhook in-flight | spec duración; default 10s |
| postgres | 10s | checkpoint corto | default Compose |
| caddy | 10s | drain HTTP | default Compose |
| migrate (profile) | 5s | one-shot | no copies 60s |
docker stop Linux | 10s default | daemon | CLI stop |
docker stop Windows | 30s default | daemon | CLI stop |
docker stop -t -1 | infinito | no en CI | CLI stop |
| Fly | 5s default, max 300s | kill_timeout | fly.toml |
| Fly señal default | SIGINT | no TERM | fly.toml |
| Railway | 0s default | RAILWAY_DEPLOYMENT_DRAINING_SECONDS | docs deploy |
| exit 137 | 128+9 SIGKILL | grace corto o sin handler | Unix + CLI SIGKILL |
Errores comunes

| Síntoma | Causa | Fix |
|---|---|---|
| exit 137 en deploy | default 10s | stop_grace_period |
| deploy “cuelga” 5 min | 300s absurdo / Fly max | mide p95 tool |
| SIGTERM no llega | PID 1 Node | tini |
| Fly yaml grace | copy Compose | kill_timeout + kill_signal |
| stop -t 1 en scripts | override CLI | alinea con yaml |
| Railway corte a 0s | default drain | RAILWAY_DEPLOYMENT_DRAINING_SECONDS |
| Fly mata “ya” | default SIGINT | kill_signal = "SIGTERM" |
--timeout -1 en CI | espera infinita | número finito |
Relación con el resto
- Código: graceful shutdown.
- PID 1: tini.
- Orden down: depends_on — Compose para primero el agente, luego DB, si declaras depends.
- Health: healthchecks no sustituye el drain.
Checklist
-
stop_grace_perioden agent ≥ p95 tool + 5s (cap 60s salvo batch) - Handler SIGTERM real
- tini /
init: true - Scripts
stop -t/down --timeoutalineados al yaml - Ensayo:
compose stop agentsin 137 - Postgres no espera 60s inútil
- Fly:
kill_signalTERM +kill_timeoutmedido (≤300) - Railway: drain > 0 si hay tools in-flight
FAQ
¿Workers? Isolates; no hay SIGTERM de contenedor.
¿Vercel? Timeout de function, no Compose.
¿30s vs 60s? Mide. 60s atrasa deploys. Fly te deja hasta 300s; no es una invitación.
El ensayo: webhook lento (sleep 15). compose stop; el handler termina; exit 0. Sin grace: 137.
No pongas 10m “por si acaso”. El orquestador espera. Techo: p99 + margen. Spec admite 1m30s; el problema no es la sintaxis, es el rolling.
Qué no hacer
No subas grace y dejes server.close() sin implementar. No uses stop_signal: SIGKILL “para que pare ya”: pierdes el drain. No copies 30s a postgres: alargas compose down en cada deploy.
Scripts de CI con docker compose down --timeout 2 pisan el yaml. El flag gana. Alinea --timeout al mismo número.
Deploy rolling
compose up -d recrea: stop del viejo + start del nuevo. Si grace es 60s, el rolling es 60s+health. depends_on healthy espera postgres, no el drain del agente.
El ensayo extra: time compose stop agent ≈ tu grace si el handler se cuelga; si el handler es correcto, mucho menos.
stop_signal
Default SIGTERM (spec). stop_signal: SIGINT solo si tu proceso ya lo trata (Ctrl+C local). Un solo contrato: TERM → close → exit 0.
Dockerfile STOPSIGNAL y docker run --stop-signal son el mismo canal que el CLI documenta para docker stop. En Compose, el campo del servicio es el que gana para compose stop.
Si usas un wrapper shell sin exec, el signal no llega: otra vez tini o exec node.
Telegram reintenta webhooks si cortas con 5xx. SIGKILL = timeout del cliente + retry. Drain bien = 200 y no doble tool.
Qué medir
p50/p95 de duración de tools en logs. Grace = p95 + 5–10s, cap 60s salvo jobs batch (esos van profile aparte, no el webhook).
Siguiente paso: si ya hay 30s y igual 137, el proceso no ve SIGTERM → tini. Sin runtime: curso.
Lecturas relacionadas
Sigue explorando Deploy y otras piezas para builders.



