MAATRIX / Блог / Телеграм-бот замолчал и не упал: разбор тихой смерти

Телеграм-бот замолчал и не упал: разбор тихой смерти

MAATRIX

В мониторинге всё зелёное, systemctl status показывает active (running) уже третьи сутки, а в личке накопилось два десятка сообщений от пользователей: бот не отвечает. Это одна из самых неприятных категорий сбоев в продакшене — процесс жив, в логах ни одной строчки об ошибке, всё выглядит штатно, а функциональность при этом мертва. Разберём, почему Telegram-боты умирают именно так — тихо, без падения — и как построить проверки, которые ловят не "процесс запущен", а "бот реально отвечает".

Почему "процесс работает" ничего не гарантирует

Классическая проверка после деплоя выглядит так:

systemctl status mybot.service
● mybot.service - Telegram bot
     Loaded: loaded (/etc/systemd/system/mybot.service; enabled)
     Active: active (running) since Tue 2026-08-25 09:12:03 UTC; 3 days ago
   Main PID: 14201 (python3)

Всё, что здесь видно, — это факт: процесс с PID 14201 существует и не завершился с ошибкой на уровне ОС. Systemd не знает и не может знать, что происходит внутри интерпретатора Python — обрабатывается ли очередь обновлений, отвечает ли event loop, не завис ли конкретный await. Для systemd "жив" означает буквально "не вызвал exit()" — и всё.

Та же проблема у типового мониторинга. Если вы проверяете бота через ps aux | grep mybot в Zabbix-агенте или через триггер "процесс с таким именем существует" — вы измеряете ровно то же самое, что и systemd. А если бот работает через long polling (не webhook), у него часто вообще нет открытого порта, который можно было бы дёрнуть HTTP-чекером вроде тех, что использует Uptime Kuma для сайтов — снаружи проверять физически нечего, соединение бот инициирует сам, наружу он ничего не слушает.

В итоге получается разрыв: у вас есть мониторинг, он ничего не сигналит, а бот при этом не работает уже несколько часов. Проблема не в том, что мониторинг сломан — он честно делает то, что вы его попросили. Проблема в том, что вы просили не то.

Три типичных сценария тихой смерти

На практике "бот жив, но не отвечает" почти всегда сводится к одному из трёх сценариев.

1. Зависшее соединение с Telegram API. Long polling держит открытый HTTP-запрос к getUpdates с большим таймаутом (обычно 30-50 секунд ожидания на стороне Telegram). Если где-то по дороге — у провайдера, на балансировщике, в NAT-таблице — соединение превращается в "полуживое" (TCP-сессия формально существует, но пакеты в одну сторону не проходят), клиентская библиотека может решить, что просто ждёт ответа дольше обычного. Явного исключения не будет: сокет не разорван, просто трафика по нему больше нет. Без собственного таймаута на уровне HTTP-клиента цикл getUpdates может подвиснуть на неопределённое время.

2. Deadlock или бесконечный await в одной корутине. У asyncio один event loop на процесс. Если внутри обработчика команды есть блокирующий синхронный вызов без вынесения в отдельный поток — например прямой requests.get(url) без таймаута внутри async def handler, или синхронная работа с файлом на медленном диске — этот вызов блокирует весь loop целиком. Другой частый вариант: корутина ждёт asyncio.Lock, который никогда не освобождается, потому что в другом месте кода lock.release() не вызывается из-за необработанного исключения до finally. Результат один: процесс жив, планировщик ОС видит активный поток, а событийный цикл бота стоит колом — ни одно новое сообщение не обрабатывается.

# антипаттерн: блокирует весь event loop
async def handle_message(message):
    data = requests.get("https://slow-api.example.com/data")  # без timeout, синхронно
    await message.answer(data.text)

3. Истёкший или отозванный токен. Токен можно случайно отозвать через /revoke_token у @BotFather, потерять валидность после смены владельца бота, или он мог быть скомпрометирован и отозван вручную. Telegram в этом случае начинает отвечать 401 Unauthorized на все запросы. Если в коде обработка ошибок API написана в духе try: ... except Exception: pass (или логирование идёт на уровне DEBUG, который в проде выключен) — бот продолжает крутить цикл, дисциплинированно "работает", просто каждый запрос к API молча отбивается.

Если ваш бот, наоборот, падает и не поднимается сам после рестарта — это другой класс проблем, он разобран отдельно в статье про бота, который падает после перезапуска. Здесь речь именно про случай, когда падения нет вообще.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать VPS

Диагностика: ручная проверка вместо доверия дашборду

Первый шаг — не смотреть в мониторинг, а лично проверить функциональность за 30 секунд.

Отправьте боту тестовое сообщение из своего аккаунта и засеките время. Если это чат-бот с интерфейсом — Telegram покажет "печатает…", если бот вообще обрабатывает апдейт. Если статус "печатает" не появляется и ответа нет 10-15 секунд — функциональность мертва, дальше уже не важно, что говорит systemctl.

Проверьте валидность токена напрямую, в обход вашего кода:

curl -s "https://api.telegram.org/bot<ВАШ_ТОКЕН>/getMe"

Рабочий токен вернёт {"ok":true,"result":{"id":...,"first_name":"MyBot",...}}. Если видите {"ok":false,"error_code":401,"description":"Unauthorized"} — причина найдена, это сценарий №3.

Посмотрите не факт наличия логов, а время последней осмысленной записи:

journalctl -u mybot.service -n 50 --no-pager

Ключевой вопрос — не "есть ли там строчки", а "когда была последняя строчка про реально обработанное сообщение". Сравните это время со временем старта процесса:

ps -o pid,etime,lstart,cmd -p $(pgrep -f mybot)

Если процесс запущен три дня назад, а последняя строка в логе про обработку апдейта — от вчерашнего вечера, это прямое подтверждение тихой смерти: где-то между "вчера вечером" и "сейчас" бот перестал реагировать на входящие события, не оставив об этом ни следа.

Для подозрения на deadlock — если есть возможность поставить py-spy (pip install py-spy), можно снять дамп стека работающего процесса без его остановки:

py-spy dump --pid 14201

Вывод покажет, на какой именно строке кода зависла каждая корутина в данный момент — обычно сразу видно засевший await без движения.

Фикс по найденной причине и перезапуск

Рестарт без понимания причины — это откладывание проблемы на несколько часов или дней, не решение. Порядок действий должен быть таким: сначала диагноз, потом фикс, потом рестарт.

Если причина — зависшее соединение, добавьте явный таймаут на уровне HTTP-клиента библиотеки. Для aiogram 3 это настраивается через сессию бота:

from aiogram.client.session.aiohttp import AiohttpSession

session = AiohttpSession(timeout=60)  # секунд, ориентир — подберите под свой polling_timeout
bot = Bot(token=TOKEN, session=session)

Без явного таймаута библиотека может унаследовать поведение "ждать неограниченно", что и создаёт зависание при полуживом соединении.

Если причина — блокирующий вызов в event loop, вынесите его в отдельный поток:

import asyncio

async def handle_message(message):
    data = await asyncio.to_thread(requests.get, "https://slow-api.example.com/data", timeout=10)
    await message.answer(data.text)

Так синхронный вызов с таймаутом уходит в отдельный поток и не блокирует весь цикл событий, даже если сам внешний сервис подвиснет.

Если причина — токен, перевыпустите его через /revoke у @BotFather, обновите переменную окружения или .env-файл на сервере, и только после этого перезапускайте процесс:

systemctl restart mybot.service
journalctl -u mybot.service -f

Второй командой стоит понаблюдать несколько минут за живым логом — именно за появлением строк об обработке новых апдейтов, а не просто за отсутствием ошибок.

Health-check функциональности, а не процесса

Раз обычный мониторинг "процесс жив" ничего не гарантирует, нужна отдельная проверка именно рабочего поведения бота — по расписанию, автоматически, без ручного участия.

Идея простая: пусть сам бот при каждой успешно обработанной команде или входящем сообщении дёргает внешний dead man's switch. Для этого хорошо подходит связка с healthchecks.io — сервис, который ждёт от вас регулярный "пинг" и поднимает тревогу, если пинг не пришёл вовремя. Подробно принцип его настройки под cron-задачи разобран в статье про мониторинг через healthchecks.io, но для бота схема почти та же:

import httpx

async def on_update_processed():
    try:
        await httpx.AsyncClient().get("https://hc-ping.com/ВАШ-UUID", timeout=5)
    except Exception:
        pass  # сбой самого пинга не должен ронять обработку сообщений

Настройте на healthchecks.io период "Expected every N minutes" исходя из реальной активности вашего бота — если сообщения приходят раз в несколько минут даже в тихие часы, ставьте таймаут с запасом (например вдвое больше обычного интервала между сообщениями). Если пинг не пришёл в срок — сервис уведомит вас в Telegram, email или webhook, причём именно потому, что событий обработки не было, а не потому что процесс упал.

Более простой вариант без внешнего сервиса — синтетический тест: отдельный скрипт по cron раз в 5-10 минут отправляет боту тестовую команду через API и через getUpdates/лог проверяет, появился ли ответ в течение разумного окна (например 30-60 секунд). Это тяжелее в реализации, чем dead man's switch, но проверяет полный цикл "апдейт пришёл → обработан → ответ ушёл", а не только факт активности внутри процесса.

Watchdog: автоматический рестарт при отсутствии активности

Health-check без автоматического реагирования — это просто более честный мониторинг, который всё равно требует, чтобы кто-то среди ночи зашёл и перезапустил бота руками. Следующий шаг — привязать обнаружение тишины к автоматическому рестарту.

Systemd умеет это из коробки через встроенный watchdog-механизм, но требует, чтобы сам процесс слал heartbeat. Правится это через Type=notify и библиотеку sdnotify:

# /etc/systemd/system/mybot.service
[Service]
Type=notify
ExecStart=/opt/mybot/venv/bin/python3 /opt/mybot/main.py
WatchdogSec=180
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=600
StartLimitBurst=5
import sdnotify

notifier = sdnotify.SystemdNotifier()

async def on_update_processed():
    notifier.notify("WATCHDOG=1")

Смысл в том, что если heartbeat WATCHDOG=1 не приходит дольше WatchdogSec (в примере — 180 секунд), systemd считает сервис зависшим и убивает его сам, после чего Restart=on-failure поднимает процесс заново. Это как раз ловит сценарий deadlock из второго раздела: если event loop встал, вызов notifier.notify() внутри обработчика тоже перестаёт выполняться, и watchdog сработает автоматически, без участия внешнего мониторинга.

Обязательно добавьте StartLimitBurst и StartLimitIntervalSec — иначе при повторяющейся причине (например токен снова стал невалидным) вы получите бесконечный цикл рестартов вместо диагностируемого сбоя. Пять попыток за 10 минут — разумный ориентир, чтобы не закопать причину под шумом перезапусков, но и не считать один случайный сбой поводом останавливаться.

Если внедрять sdnotify в код пока не хочется, работает и более грубый вариант — отдельный cron-скрипт, сверяющий время последней строки в логе с текущим временем:

#!/bin/bash
LAST_LOG=$(journalctl -u mybot.service -n 1 --output=short-unix | awk '{print $1}')
NOW=$(date +%s)
if [ $((NOW - LAST_LOG)) -gt 600 ]; then
    systemctl restart mybot.service
fi

Это менее точный инструмент — он опирается на наличие логов, а не на прямое подтверждение обработки апдейта — но он не требует правок в самом коде бота и разворачивается за пять минут. Тот же принцип "тишина в логах = сигнал к действию, а не повод расслабиться" разобран в статье про мониторинг диска, который молчал целый месяц — там ровно та же ловушка: технически всё "работает", просто никто не проверял то, что действительно важно.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать VPS

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Как понять, использует ли мой бот polling или webhook, если код доставался по наследству?

Проверьте, вызывается ли в коде getUpdates в цикле (polling) или настроен ли setWebhook с внешним URL (webhook). Быстрая проверка через API: curl "https://api.telegram.org/bot<ТОКЕН>/getWebhookInfo" — если url в ответе пустой, бот работает через polling.

Сколько времени без активности считать поводом для тревоги?

Единого числа нет — ориентируйтесь на реальный профиль нагрузки вашего бота. Если сообщения приходят каждые 1-2 минуты даже ночью, порог в 10 минут тишины уже подозрителен. Если бот используется несколько раз в день, ставьте порог с большим запасом — иначе будете получать ложные тревоги в каждое затишье.

Может ли watchdog-рестарт зациклиться, если причина не устранена?

Да, если проблема системная (например токен остался невалидным), рестарт просто повторит тот же сбой через WatchdogSec секунд. Именно поэтому важны StartLimitBurst и StartLimitIntervalSec — они переводят бесконечный цикл в понятный отказ сервиса, который вы увидите через systemctl status, а не бесконечные тихие рестарты.

Нужен ли для health-check и watchdog отдельный сервер?

Нет, оба варианта — и sdnotify, и внешний dead man's switch на healthchecks.io — работают в пределах того же VPS, где крутится бот, и почти не расходуют ресурсы. Отдельная машина имеет смысл только если вы хотите проверять доступность именно с внешней точки, независимо от состояния самого сервера бота.

Что делать, если после фикса бот всё равно иногда "затихает" без видимой причины?

Соберите дампы через py-spy dump в момент следующего зависания — почти всегда за этим стоит конкретный блокирующий вызов без таймаута, который не был пойман в первый раз. Полностью необъяснимых зависаний на практике почти не бывает — обычно просто не с той стороны посмотрели.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →