Python-разработчик держал бота на aiogram, около 4000 сообщений в день. В часы пик бот падал каждые два с небольшим часа. Причина обнаружилась не сразу: без троттлинга входящих сообщений пять-десять одновременных обращений от одного пользователя запускали столько же параллельных исходящих вызовов. Бот упирался в flood control Телеграма, обработчики зависали на повторных попытках и не отпускали HTTP-сессии. На пике их скопилось 47 штук одновременно, память росла, и процесс убивал системный OOM killer – просто потому что ему не хватало ресурсов.
Автор описал это в блоге на dev.to: фикс занял 18 строк middleware с TTL-кэшем, который отбрасывает повторное сообщение от того же пользователя в течение секунды. После этого ошибки TelegramRetryAfter исчезли за четыре часа наблюдения, а число висящих сессий упало с сорока с лишним до одной-двух.
Показательно в этой истории именно время, которое ушло на поиск причины. Бот падал, поднимался заново – иногда сам, иногда вручную – и продолжал падать по той же причине, потому что никто не смотрел на паттерн. Для владельца бизнеса, который держит на VPS своего ИИ-агента – Telegram-бота, обработчик заявок, автоматизацию на связке с LLM API – это ровно тот сценарий, который отличает рабочий сервис от красивой демки на локальной машине. Ниже – минимальный набор, который ловит такие падения и поднимает процесс автоматически, без Prometheus и Grafana ради одного бота.
Почему бот падает молча
Чаще всего к этому приводит один из трёх сценариев. Как в истории выше, всплеск одновременных запросов роняет процесс в рейт-лимит внешнего API – Telegram, LLM-провайдера, не важно. Если счётчик повторов и бэкоффа при этом живёт только в оперативной памяти процесса, повторный запуск обнуляет эту память подчистую. На Hacker News разработчик описывал случай, когда именно так и произошло: сервис перезапустился, счётчик троттлинга сбросился, и наружу улетело примерно в 100 000 раз больше запросов к внешнему API, чем предполагалось. Голый автоперезапуск здесь только лечит симптом, потому что на следующем витке риск повторяется тот же самый.
Реже, но так же незаметно бота душит утечка памяти на длинных диалогах. История переписки с пользователем копится в оперативной памяти процесса без ограничения – для агента на LLM это обычное дело, ему нужен контекст предыдущих сообщений, – и за несколько часов активной работы память забивается тем же OOM killer'ом, только медленнее и без явного триггера в виде всплеска запросов. Внешне это выглядит как «бот просто стал тормозить, а потом упал», и без метрики потребления памяти во времени источник почти невозможно отличить от рейт-лимита на глаз.
Третий сценарий тише всех: сетевой таймаут к LLM-провайдеру или Телеграму без явной обработки в коде подвешивает процесс на неопределённое время. Он ничего не отвечает, но формально и не падает – с точки зрения systemd или Docker процесс жив, просто не работает, и именно поэтому его пропускает даже простой автоперезапуск.
Причину падения мониторинг сам по себе не устраняет – зато снимает слепоту к паттерну: показывает частоту, и дальше с этим уже можно работать руками. Бот, который упал раз за сутки, – просто баг, с которым можно жить. Тот же бот, падающий каждые два часа стабильно в пиковые нагрузки, сигнализирует о системной проблеме, только увидеть эту периодичность можно, если кто-то – или что-то – считает падения вместо того, чтобы вслепую поднимать процесс заново после каждого.
Минимальный набор: автоперезапуск, healthcheck, алерт
Для одного бота на VPS не нужен полноценный observability-стек. Разработчики, которые пробовали ставить Prometheus и Grafana ради мониторинга пары Raspberry Pi, сами потом называли это в блогах оверинжинирингом. Хватает трёх слоёв, и каждый решает свою часть задачи.
Автоперезапуск. Если процесс запущен через systemd, в unit-файле достаточно пары строк:
[Service]
ExecStart=/usr/bin/python3 /home/deploy/bot/main.py
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=300
StartLimitBurst=10
Restart=on-failure поднимает процесс после падения, RestartSec даёт пять секунд паузы вместо мгновенного зацикливания. Две последние строки важны отдельно: без них процесс, который падает по детерминированной причине (ошибка конфига, нехватка прав, кончившееся место на диске), уйдёт в бесконечный цикл рестартов. StartLimitBurst останавливает попытки после десяти падений за пять минут – дальше в дело вступает уже человек, автоматика на этом останавливается.
Если бот в Docker-контейнере, здесь есть тонкость: политика restart: unless-stopped перезапускает контейнер только по факту его завершения – по exit code. Статус HEALTHCHECK unhealthy в эту логику не входит вообще, поэтому без явной связки между ними контейнер может висеть в состоянии unhealthy сколь угодно долго: формально процесс внутри не завершился, и триггера для рестарта попросту нет.
Healthcheck. Автоперезапуск отвечает на вопрос «упал ли процесс», но не отвечает на вопрос «жив ли он на самом деле» – процесс может работать и ничего не делать (завис на сетевом таймауте, к примеру). Здесь помогает внешний пинг по принципу dead man's switch: сервис вроде healthchecks.io выдаёт уникальный URL, и скрипт или systemd-таймер дергает его раз в N минут через curl. Если пинг не пришёл вовремя – сервис считает, что что-то не так, и шлёт алерт. Бесплатного тарифа (20 проверок) для одного-двух ботов достаточно с запасом.
Алерт себе. Отдельного сервиса можно избежать вообще – самому боту (или systemd через ExecStopPost) достаточно одного curl к api.telegram.org/bot/sendMessage, чтобы упавший процесс успел отправить сообщение перед перезапуском. Дешевле и без внешней зависимости, но такое уведомление работает только если у процесса ещё есть шанс отправить сообщение до смерти – при OOM killer это не гарантировано, и внешний пинг остаётся более надёжным вариантом.
Отдельно стоит логирование: без ротации логи за пару недель забивают диск на минимальном тарифе VPS, и от переполненного диска в итоге падает уже вся система целиком, вместе с ботом. logrotate с конфигом в /etc/logrotate.d/ закрывает это за десять минут настройки:
/home/deploy/bot/logs/*.log {
daily
rotate 14
compress
missingok
notifempty
}
Четырнадцать сжатых копий с ротацией раз в день – для одного бота этого достаточно с большим запасом, и диск больше не заполнится незаметно.
Разница между сервисами для healthcheck-пинга в основном в бесплатном лимите. У healthchecks.io бесплатный тариф – 20 проверок и до сотни записей истории на каждую, карта не требуется. У Better Stack (бывший Better Uptime) бесплатно – 10 мониторов и 10 heartbeat-проверок с интервалом в три минуты, и что удобно – Telegram-алерты уже встроены, не нужно городить свой curl. У UptimeRobot исторически был щедрый бесплатный тариф на 50 мониторов, но в 2025 году сервис резко поднял цены на платные планы, и часть пользователей на форумах жаловалась на сюрприз при попытке расширить лимиты – держать единственный канал алертов на бесплатном тарифе стороннего сервиса стоит с оглядкой именно на этот прецедент.
Когда простого набора мало
Три слоя выше закрывают процентов восемьдесят типичных причин простоя одного бота. Не закрывают они одно – те самые детерминированные падения, которые уходят в цикл рестартов, если не поставить StartLimitBurst. И не закрывают ситуацию, где счётчик состояния (тот самый троттлинг из истории про Hacker News) живёт только в памяти процесса: тут после каждого рестарта система стартует «с чистого листа» и рискует повторить тот же всплеск, который её и уронил. Если такое поведение критично, состояние стоит выносить во внешнее хранилище – Redis или файл на диске – чтобы оно переживало перезапуск.
Полноценный observability-стек имеет смысл заводить не раньше, чем ботов или сервисов на сервере становится несколько и вручную уже не уследить за паттернами падений по логам. До этого момента три описанных слоя дают ровно то, что нужно: узнать о падении раньше пользователя и подняться самостоятельно, не тратя вечер на разбор дампа памяти.
Частые вопросы
Хватит ли просто Restart=always без остального? Само по себе – нет. Это чинит только «процесс умер», но ничего не говорит о зависшем процессе и не защищает от цикла рестартов при детерминированной ошибке. StartLimitBurst и внешний healthcheck закрывают ровно то, что автоперезапуск не видит.
Нужен ли Docker для такого набора? Нет, systemd unit-файл работает и для голого Python-процесса без контейнера. Docker добавляет свою логику healthcheck, но требует отдельно связать её с политикой перезапуска – иначе появляется описанный выше разрыв между «упал» и «завис».
Сколько времени занимает настройка с нуля? Unit-файл с автоперезапуском – минут пятнадцать, регистрация в healthchecks.io и добавление пинга – ещё столько же, ротация логов – минут десять. Реалистично уложиться в один вечер, если сервер уже поднят и приложение на нём уже работает.
А если бот работает на pm2 вместо systemd? Логика та же: автоперезапуск из коробки есть, healthcheck и алерты придётся навешивать так же отдельно. pm2 удобнее для Node.js-процессов, но добавляет заметный оверхед по памяти против голого systemd – для одного маленького бота это может быть избыточно.
Во сколько обходится такой набор? При бесплатных тарифах – в ноль, кроме самого VPS. systemd и logrotate идут в составе Linux бесплатно, healthchecks.io или Better Stack на бесплатном тарифе закрывают одного-двух ботов без карты. Разница появляется только если ботов становится больше десятка или нужен интервал проверки короче минуты – тогда платные тарифы стартуют от 20-30 долларов в месяц, и здесь уже стоит сравнивать конкретные лимиты под свою нагрузку.
Заключение
Разница между ботом, который просто работает, и ботом, который работает без присмотра, обычно сводится к трём скучным вещам вокруг кода самого агента: перезапуску, проверке живости и уведомлению. Все три ставятся за один вечер и без единого платного сервиса, если не считать бесплатных тарифов. Дальше можно спать спокойно, зная, что если бот всё-таки упадёт в три ночи, кто-то – то есть что-то – заметит это раньше, чем первый недовольный пользователь.
Комментарии