Qué pasa cuando el hosting detiene tu bot
Docker, systemd, Kubernetes y la mayoría de las plataformas siguen la misma secuencia:
- Envían SIGTERM al proceso principal: "por favor, detente".
- Esperan un periodo de gracia.
- Envían SIGKILL si el proceso sigue vivo. SIGKILL no se puede capturar; el proceso muere a mitad del trabajo.
Periodos de gracia por defecto según la documentación oficial:
| Entorno | Espera antes de SIGKILL | Ajuste |
|---|---|---|
docker stop |
10 segundos | --time, o stop_grace_period en Compose |
| systemd | 90 segundos (DefaultTimeoutStopSec) |
TimeoutStopSec= en la unidad |
| Kubernetes | 30 segundos | terminationGracePeriodSeconds |
Las plataformas administradas tienen sus propios valores; revisa la documentación de tu hosting y dale a tu código de apagado unos segundos menos de lo que permita.
Qué falla sin un handler
- Respuestas duplicadas. Arranca una instancia nueva mientras la vieja sigue conectada. Con long polling de Telegram ambas compiten por los updates (la segunda recibe 409 Conflict); con Discord ambas sesiones reciben eventos y las dos responden.
- Trabajo perdido. Llegó un mensaje, la respuesta iba a medias y SIGKILL la cortó.
- Updates repetidos. Un bot de polling que no confirmó su último offset recibe los mismos updates tras reiniciar.
- Archivos corruptos. Una escritura de SQLite o un archivo JSON de estado interrumpido a la mitad.
La receta de apagado
Al recibir SIGTERM:
- Deja de aceptar trabajo nuevo (detén el polling, cierra la conexión al gateway, que el servidor HTTP no acepte conexiones).
- Deja terminar los handlers en curso, con un tiempo límite.
- Vacía los logs, cierra la base de datos, confirma los mensajes de la cola.
- Sal con código 0.
Node.js
grammY (long polling):
import { Bot } from "grammy";
const bot = new Bot(process.env.BOT_TOKEN);
// ... handlers
const stop = () => bot.stop(); // termina el lote actual y luego resuelve start()
process.once("SIGINT", stop);
process.once("SIGTERM", stop);
await bot.start();
await db.close(); // corre cuando el polling ya se detuvo
discord.js:
async function shutdown() {
console.log("Apagando");
await client.destroy(); // cierra la conexión al gateway
await db?.close();
process.exit(0);
}
process.once("SIGTERM", shutdown);
process.once("SIGINT", shutdown);
Servidores de webhook (Express, Fastify, Hono en Node): llama a server.close() para dejar de aceptar conexiones y deja que terminen las peticiones en curso antes de salir.
Python
python-telegram-bot lo resuelve por ti: Application.run_polling() y run_webhook() escuchan SIGINT, SIGTERM y SIGABRT por defecto (el parámetro stop_signals), dejan de pedir updates y ejecutan tu hook post_shutdown:
async def post_shutdown(app):
await app.bot_data["db"].close()
app = Application.builder().token(TOKEN).post_shutdown(post_shutdown).build()
app.run_polling()
discord.py: registra un handler de SIGTERM que cierre el cliente.
import asyncio, signal
async def main():
async with client:
loop = asyncio.get_running_loop()
loop.add_signal_handler(signal.SIGTERM, lambda: asyncio.create_task(client.close()))
await client.start(TOKEN)
asyncio.run(main())
loop.add_signal_handler funciona en Unix, que es lo que corren las imágenes de Docker y los servidores Linux.
La trampa del PID 1 en Docker
Las señales van al PID 1 del contenedor. Si tu Dockerfile usa la forma shell:
CMD python -m bot.main # /bin/sh es el PID 1
entonces /bin/sh recibe SIGTERM y puede no reenviarlo, tu handler nunca corre y Docker mata el bot a los 10 segundos. Usa la forma exec:
CMD ["python", "-m", "bot.main"]
Arrancar con npm start agrega otro proceso intermedio; prefiere CMD ["node", "index.js"]. Si tu proceso lanza procesos hijos, corre el contenedor con --init (o init: true en Compose) para que un init mínimo reenvíe señales y recoja procesos zombis. El Dockerfile completo está en desplegar un bot en Python con Docker.
systemd
[Service]
ExecStart=/opt/bot/.venv/bin/python -m bot.main
Restart=always
KillSignal=SIGTERM
TimeoutStopSec=20
Restart=always levanta el bot tras un fallo; TimeoutStopSec acota cuánto puede tardar en detenerse. Configuración completa en el tutorial de VPS en Vultr con systemd.
Despliegues sin solapamiento
Para bots de polling y gateway, la regla es una sola instancia corriendo. Si tu plataforma arranca la versión nueva antes de detener la vieja, desactiva el solapamiento para ese servicio o haz que la nueva espere a que la vieja se desconecte. Los bots con webhook son más fáciles: varias instancias pueden atender peticiones a la vez, así que los despliegues graduales son seguros mientras los handlers toleren duplicados. Más en mantener un bot en línea 24/7 y la guía de confiabilidad y límites.
Preguntas frecuentes
¿Cómo pruebo mi handler de apagado?
Corre el bot en Docker y ejecuta docker stop <contenedor>. Revisa que aparezca tu mensaje de apagado en los logs y que tarde menos que el periodo de gracia. En local, kill -TERM <pid> hace lo mismo.
¿El bot debería avisar "me desconecto"?
Normalmente no. Los reinicios son frecuentes y cortos; los avisos son ruido. Mejor regístralo en los logs.
¿Qué código de salida uso?
Sal con 0 tras un apagado solicitado. Un código distinto de cero le dice al supervisor que algo falló, lo que puede disparar alertas o esperas entre reinicios.
Fuentes
- Docker docs, docker container stop: docs.docker.com/reference/cli/docker/container/stop
- Docker docs, CMD y forma exec en Dockerfile: docs.docker.com/reference/dockerfile
- systemd, systemd-system.conf (DefaultTimeoutStopSec): freedesktop.org/software/systemd/man/latest/systemd-system.conf.html
- Kubernetes docs, terminación de pods: kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle
- python-telegram-bot, Application.run_polling: docs.python-telegram-bot.org
- grammY, long polling y detener el bot: grammy.dev/guide/deployment-types