Когда Telegram-бот на webhook перестаёт получать обновления, ответ почти всегда лежит в одном вызове API: getWebhookInfo. Он показывает, на какой URL Telegram собирается доставлять обновления, какой IP-адрес он разрешил, сколько обновлений стоит в очереди и, что полезнее всего, последнюю ошибку, которую вернул ваш сервер, - обычным текстом. Прочитайте url, pending_update_count и last_error_message вместе, и станет понятно, что из четырёх вещей сломано: регистрация, TLS, HTTP-ответ или скорость вашего обработчика. Исправьте что-то одно, и накопившиеся обновления разгребутся сами.
Эта статья - вторая половина темы, посвящённая диагностике. Выбор между polling и webhook, настройки BotFather, код библиотек и развёртывание разобраны в статье Хостинг Telegram-бота: polling и webhook; здесь webhook уже установлен, но что-то не доходит.
Начните с getWebhookInfo#
Метод не принимает параметров и работает с любой машины, где есть токен, так что его можно вызвать с ноутбука, пока бот работает в другом месте:
$ curl -s "https://api.telegram.org/bot$BOT_TOKEN/getWebhookInfo"Сломанный webhook выглядит примерно так (отформатировано для чтения):
{ "ok": true, "result": { "url": "https://bot.example.com/tg/hook", "has_custom_certificate": false, "pending_update_count": 37, "ip_address": "203.0.113.10", "last_error_date": 1760511600, "last_error_message": "Wrong response from the webhook: 404 Not Found", "max_connections": 40, "allowed_updates": ["message", "callback_query"] }}Поля в том виде, как их определяет Bot API:
| Поле | Что оно говорит |
|---|---|
url | Куда Telegram доставляет обновления. Пусто - webhook нет, бот работает в режиме polling |
has_custom_certificate | true, если вы загрузили сертификат через setWebhook |
pending_update_count | Обновления, ожидающие доставки |
ip_address | Адрес, на который Telegram действительно отправляет запросы |
last_error_date | Unix-время последней неудачной доставки |
last_error_message | Текст этой ошибки, понятный человеку |
last_synchronization_error_date | Ошибка синхронизации внутри Telegram, не на вашей стороне |
max_connections | Допустимое число одновременных доставок, 1-100 |
allowed_updates | Типы обновлений, на которые подписан бот |
Необязательные поля просто пропадают, когда сказать нечего, поэтому у исправного webhook вообще нет ключей last_error_* или там только старая дата. last_error_date - это секунды с начала эпохи; в Linux команда date -d @1760511600 превращает её в читаемую дату, и первым делом стоит сравнить её с текущим временем. Ошибка трёхдневной давности при нулевом pending_update_count - это история, а не проблема.
Читаем поля вместе#
Ни одно поле само по себе ничего не диагностирует. Диагноз дают сочетания:
| Что вы видите | Что это значит |
|---|---|
url пустой | Webhook не зарегистрирован. Кто-то вызвал deleteWebhook или его сбросил polling-процесс |
url неверный или устаревший | Устаревшая регистрация: старое имя хоста, старый путь, URL тестового окружения |
| Счётчик растёт, ошибка свежая | Telegram не может доставить обновление и повторяет попытки |
| Счётчик растёт, свежей ошибки нет | Доставка проходит, но ваш обработчик слишком медленный |
| Счётчик нулевой, ошибок нет, ответов нет | Обновления приходят; ошибка в вашем коде или в allowed_updates |
Первая строка ловит больше людей, чем должна. Некоторые библиотеки вызывают deleteWebhook, когда вы запускаете их в режиме polling, поэтому один запуск бота локально "просто проверить" с боевым токеном незаметно удаляет боевой webhook. Сам он не вернётся, пока кто-нибудь снова не вызовет setWebhook. Если ваш деплой регистрирует webhook только при смене URL, он может не вернуться вообще.
Последняя строка - тот случай, когда getWebhookInfo не поможет, потому что со стороны Telegram всё в порядке: он отправил обновление, ваш сервер ответил 200, на этом всё. Сначала проверьте allowed_updates - если там только message, нажатия кнопок (callback_query) не приходят, а обновления chat_member отправляются, только если запросить их явно. Затем проверьте логи: исключение, проглоченное уже после отправки 200, выглядит точно так же, как тишина.
На ip_address стоит смотреть каждый раз. Это адрес, в который Telegram разрешил ваше имя хоста, и если это не тот сервер, который вы имеете в виду, у вас проблема с DNS: A-запись всё ещё указывает на старый хост либо запись не успела распространиться. Webhook-доставка Telegram работает только по IPv4 - в руководстве по webhook прямо сказано, что IPv6 не поддерживается, - поэтому имя хоста только с AAAA-записью принимать webhook не может вовсе. Как проверить, какие записи у имени есть на самом деле, описано в статье DNS-записи простыми словами.
Ошибки SSL#
Любой last_error_message, начинающийся с SSL error, означает, что Telegram дошёл до вашего адреса, но отказался от TLS-рукопожатия. В тексте обычно есть строка ошибки OpenSSL, например certificate verify failed, а точная формулировка менялась вместе с версией OpenSSL у Telegram, так что ориентируйтесь на смысл, а не на код.
Опубликованные требования Telegram коротки и строги:
- TLS 1.2 или новее. SSLv2, SSLv3, TLS 1.0 и 1.1 отклоняются.
- CN или один из SAN сертификата должен совпадать с именем хоста в URL webhook.
- Сервер должен отдавать полную цепочку - ваш сертификат и все промежуточные.
- Самоподписанный сертификат работает, только если загрузить его параметром
certificateвsetWebhook; после этогоhas_custom_certificateпоказываетtrue. - TLS обязателен на любом разрешённом порту, включая 80. Порт 80 разрешён, обычный HTTP - нет.
Классическая причина - отсутствующий промежуточный сертификат. Браузеры часто скрывают эту проблему, потому что кэшируют промежуточные сертификаты или подтягивают их сами, поэтому сайт отлично открывается в Chrome, а Telegram, который не делает ни того ни другого, его отклоняет. Проверьте так, как это сделал бы строгий клиент:
$ openssl s_client -connect bot.example.com:443 -servername bot.example.com </dev/null \ | grep -E "Verify return code|subject=|issuer="Нужен результат Verify return code: 0 (ok). Код 21 (unable to verify the first certificate) означает отсутствие цепочки: в nginx это значит, что ssl_certificate указывает на сам сертификат, а не на файл с полной цепочкой. Certbot создаёт оба файла, нужен fullchain.pem. Просроченный сертификат даёт код 10, а несовпадение имени хоста видно в строке subject=. Подробно каждый из этих случаев разобран в статье Ошибки SSL-сертификатов простыми словами.
Одна менее очевидная причина: самоподписанный сертификат, загруженный несколько месяцев назад, а затем заменённый настоящим без повторной регистрации. Telegram по-прежнему держит старый. Вызовите setWebhook ещё раз без параметра certificate, и has_custom_certificate снова станет false.
Wrong response from the webhook#
Wrong response from the webhook: <status> означает, что TLS-рукопожатие прошло и ваш сервер ответил, но не кодом 2xx. Telegram считает сбоем всё, что вне 2xx, и повторяет доставку. Подсказка - статус в сообщении:
| Статус | Обычная причина |
|---|---|
404 Not Found | Путь в зарегистрированном URL не совпадает с маршрутом |
401 / 403 | Ваша проверка секретного токена отклоняет Telegram (см. ниже) |
405 Method Not Allowed | Маршрут зарегистрирован для GET, а Telegram отправляет только POST |
301 / 302 / 308 | Редирект, который Telegram считает сбоем |
500 Internal Server Error | Ваш обработчик упал до ответа |
502 / 503 / 504 | Прокси работает, а приложение за ним нет |
404 почти всегда вызван слешем в конце, префиксом или путём с токеном, который перестал совпадать после отзыва токена. Сравните url из getWebhookInfo с маршрутом в коде посимвольно. Библиотеки, которые строят маршрут из опции url_path, а URL - из отдельной опции webhook_url, позволяют легко изменить одно и забыть про другое.
Редиректы приходят со слоя перед вашим приложением: перенаправление с голого домена на www или наоборот, прокси, который отправляет один префикс пути в другое место, или фреймворк, дописывающий слеш в конце. Регистрируйте точный конечный URL - тот, который отвечает без редиректа.
Группа 5xx делится чётко. 500 пришёл из вашего кода, значит трассировка стека лежит в логе приложения. 502 пришёл от обратного прокси, а это значит, что приложение не запущено, упало при старте или слушает не там, где его ищет прокси. Чаще всего это приложение, привязанное к 127.0.0.1 внутри контейнера, пока прокси подключается снаружи, либо приложение на жёстко заданном порту, который не совпадает с выделенным тарифом. Почему прокси и приложение могут расходиться во мнениях о том, где находится приложение, объясняет статья Что на самом деле делает обратный прокси.
Ещё один источник ошибок 500, который выглядит как проблема Telegram: обработчик, падающий на одном конкретном обновлении. Telegram повторяет его, обработчик снова падает, и поскольку это обновление всё время не проходит, очередь за ним растёт. Одно некорректное сообщение может остановить бота, пока это обновление не будет сброшено. Оберните обработку обновления так, чтобы исключение записывалось в лог, а запрос всё равно получал свой 200.
Таймауты и ошибки соединения#
Когда сообщение говорит о соединении, а не о статусе, Telegram вообще не получил HTTP-ответа. Формулировки различаются, но семейства такие:
- Connection refused - адрес разрешился, и в ответ сообщили, что на этом порту никто не слушает. Прокси не запущен или порт из URL не открыт.
- Connection timed out - пакеты ушли в никуда. Файрвол отбрасывает трафик, неверный IP или домашний роутер без проброса порта.
- Read timeout expired - соединение открылось, но ваш сервер отвечал слишком долго. Это медленный обработчик, а не проблема сети.
- Failed to resolve host - у имени хоста нет A-записи, которую может использовать Telegram.
Telegram не публикует свой таймаут доставки, так что не подстраивайтесь под какое-то число. Сделайте так, чтобы ответ занимал миллисекунды в любом случае.
Если вы ограничиваете входящий трафик по адресу источника, в руководстве Telegram по webhook перечислены диапазоны, с которых идёт доставка: 149.154.160.0/20 и 91.108.4.0/22. Там же сказано, что они могут измениться, поэтому список разрешённых адресов, составленный по ним, стоит перепроверять, когда доставки вдруг начинают уходить в таймаут. Большинству лучше подходит секретный токен, чем белый список IP. Об этом компромиссе рассказано в статье Правила файрвола, которые действительно важны.
Порты и ошибки setWebhook#
Webhook доставляются ровно на четыре порта: 443, 80, 88 и 8443. Не 3000, не 5000, не 8080. setWebhook отклоняет любой другой порт ошибкой bad webhook ещё при регистрации, так что если вы видите эту ошибку, до сбоя доставки дело не дошло. Другие отказы при регистрации: URL не по HTTPS и имя хоста, которое не разрешается.
Порт в URL - это порт, к которому подключается Telegram, а не порт, который слушает ваше приложение. Обычная схема - обратный прокси завершает TLS на 443 и передаёт запрос на собственный порт приложения:
Каждая стрелка соответствует своему семейству last_error_message, поэтому это поле так быстро сужает поиск. На тарифах RE:NODE для приложений такая схема - это слот прокси: направьте A-запись на показанный адрес, и сертификат будет выпущен и продлён автоматически в пределах 21-дневного окна, с TLS на 443 и передачей запроса на порт, который слушает ваше приложение. Приложение по-прежнему должно слушать 0.0.0.0 и порт тарифа; эта часть остаётся за вами. Как запустить прокси самостоятельно на своей машине, описано в руководстве по обратному прокси nginx.
Параметр ip_address в setWebhook позволяет указать Telegram фиксированный IP для отправки вместо разрешения имени хоста. TLS при этом по-прежнему проверяется по имени хоста. Это удобно, когда DNS в процессе смены и ждать нельзя, и ловушка, если вы забыли о нём и позже перенесли бота: новая A-запись тогда игнорируется. getWebhookInfo показывает тот адрес, который используется.
Очередь и drop_pending_updates#
Пока доставка не проходит, обновления копятся. Telegram хранит недоставленные обновления до 24 часов, повторяет доставку, и когда вы устраните причину, накопившееся приходит сразу потоком - в пределах max_connections, по умолчанию это 40 одновременных запросов. Из этого следуют два неожиданных эффекта. Бот, который был сломан ночью, в течение секунд после исправления отвечает на всю ночную переписку, отвечая на разговоры, закончившиеся несколько часов назад. А на небольшом сервере 40 одновременных доставок могут создать достаточную нагрузку, чтобы вызвать вторую волну таймаутов.
Когда очередь не нужна, выбросьте её. И setWebhook, и deleteWebhook принимают drop_pending_updates:
$ curl -s "https://api.telegram.org/bot$BOT_TOKEN/setWebhook" \ -d "url=https://bot.example.com/tg/hook" \ -d "secret_token=$WEBHOOK_SECRET" \ -d "max_connections=10" \ -d "drop_pending_updates=true"Сразу после этого pending_update_count должен показывать 0. Учтите, что setWebhook заменяет всю конфигурацию целиком: если при повторной регистрации не указать secret_token или allowed_updates, они пропадут. Всегда передавайте весь нужный набор.
Не сбрасывайте очередь рефлекторно. Для бота, который записывает заказы, платежи или отправленные формы, эти обновления в очереди - настоящая работа, и правильный ход - устранить неисправность, снизить max_connections до значения, с которым справляется ваш сервер, и дать очереди разгрестись. Следите за тем, как счётчик падает, повторными вызовами getWebhookInfo.
Заголовок secret_token#
secret_token - это выбранная вами строка из 1-256 символов A-Z, a-z, 0-9, _ и -, которую передают в setWebhook. После этого Telegram отправляет её в заголовке X-Telegram-Bot-Api-Secret-Token при каждой доставке, а ваш обработчик сравнивает значение и отклоняет всё, что не совпало. Это дешёвый и правильный способ убедиться, что кормить бота обновлениями может только Telegram.
Он же порождает характерный сбой. Ваш обработчик возвращает 401 или 403, last_error_message показывает Wrong response from the webhook: 403 Forbidden, а причина - одна из трёх:
- Секрет сменили в окружении, но
setWebhookс новым значением не вызывали. - При повторной регистрации
secret_tokenпропустили, и Telegram теперь вообще не отправляет заголовок. - Промежуточный прокси удаляет незнакомые заголовки. Бывает редко, но стоит проверить, если первые две причины исключены.
Исправление для всех трёх одно: задайте переменную, вызовите setWebhook с тем же значением и убедитесь, что окружение и Telegram согласны друг с другом. В getWebhookInfo секрет не виден, так что единственное доказательство - успешная доставка. Храните его рядом с токеном, а не в репозитории: причины описаны в статье Переменные окружения и секреты.
Сначала ответ, потом обработка#
Если pending_update_count растёт без ошибок или вы видите read timeout, ваш обработчик делает свою работу до того, как ответит. Каждая доставка занимает один из слотов max_connections, пока вы не ответите. Обработчик, который вызывает внешний API, пишет в базу данных и отправляет два сообщения, прежде чем вернуть 200, может работать две секунды; при всплеске сообщений слоты заполняются, доставки встают в очередь, а некоторые в итоге уходят в таймаут и повторяются, что добавляет нагрузки.
Исправление структурное: проверьте секрет, передайте обновление кому-то другому, ответьте 200 и выполняйте работу потом.
import asyncio, osfrom fastapi import FastAPI, Request, Responseapp = FastAPI()SECRET = os.environ["WEBHOOK_SECRET"]queue: asyncio.Queue = asyncio.Queue()@app.post("/tg/hook")async def hook(request: Request): if request.headers.get("X-Telegram-Bot-Api-Secret-Token") != SECRET: return Response(status_code=403) await queue.put(await request.json()) return Response(status_code=200)async def worker(): while True: update = await queue.get() try: await handle(update) # your bot logic except Exception as exc: print("update failed:", exc, flush=True)@app.on_event("startup")async def start_worker(): asyncio.create_task(worker())Очередь в памяти теряет всё, что в ней лежит, при перезапуске процесса; для чат-бота это допустимо, а для всего, что принимает платежи, нет. Если работа должна пережить перезапуск, поместите её в настоящую очередь; варианты, подходящие под один тариф, разобраны в статье Фоновые задачи на небольшом сервере. Некоторые библиотеки, в том числе webhookCallback из grammY, сами применяют таймаут к обработчику и отвечают за вас, когда время выходит, - это лучше таймаута со стороны Telegram, но это признак слишком медленного обработчика, а не решение.
Также снизьте max_connections до того, что сервер реально способен выполнять параллельно. На тарифе с 0,5 или 1 vCPU 40 одновременных доставок - это не ёмкость, а борьба за ресурсы. Для небольшого бота честным значением обычно будет от 5 до 20.
Возвращение к polling#
Иногда правильное решение - перестать использовать webhook. Если у бота несколько сотен пользователей, нет лишнего домена или вы отлаживаете и хотите видеть, как обновления приходят в терминал, polling проще и не теряет ничего важного.
$ curl -s "https://api.telegram.org/bot$BOT_TOKEN/deleteWebhook" \ -d "drop_pending_updates=false"При drop_pending_updates=false (значение по умолчанию) обновления в очереди остаются, и getUpdates доставит их при следующем опросе. После этого getWebhookInfo возвращает пустой url, и polling работает. Пока вы не удалите webhook, каждый вызов getUpdates завершается ошибкой 409 conflict - именно так люди обнаруживают, что забытый webhook всё ещё зарегистрирован. Помните и обратное: чтобы позже вернуться к webhook, нужен новый setWebhook с полным набором параметров, а деплой после этого должен проверять getWebhookInfo, а не предполагать.
Чек-лист по порядку#
Когда на дворе ночь, а бот молчит, идите по нему в таком порядке:
- Выполните
getWebhookInfo. Задан лиurlи совпадает ли он точно с URL, который обслуживает ваш код? - Это ваш сервер в
ip_address? Если нет, исправьте DNS или уберите фиксированныйip_address. - Прочитайте
last_error_date. Ошибка свежая? Если нет, проблема в вашем коде или вallowed_updates. SSL error: проверьте цепочку и имя хоста с помощьюopenssl s_client.Wrong response: статус подскажет, что это - путь, секрет, редирект или упавшее приложение.- Ошибки соединения: убедитесь, что порт - один из четырёх, прокси запущен и ничто не закрыто файрволом.
- Счётчик растёт без ошибок: сначала отвечайте, потом обрабатывайте, снизьте
max_connections. - Исправлено, а очередь устарела: зарегистрируйте заново с
drop_pending_updates=true.
Держите лог приложения открытым, пока работаете. Статья Как читать консоль сервера, не гадая поможет отделить важные ошибки от шума вокруг них.
FAQ#
Почему pending_update_count растёт, когда ошибок нет?
Telegram доставляет успешно, но ваш сервер отвечает слишком медленно и не успевает, поэтому доставки ждут свободного слота соединения. Возвращайте 200, как только запрос проверен, выполняйте работу в фоновой задаче и задайте max_connections равным тому, что ваш сервер действительно способен обработать параллельно.
Как долго Telegram хранит обновления, которые мой webhook не получил?
До 24 часов. Всё, что старше, отбрасывается независимо от того, успели ли вы починить webhook, так что бот, который не работал два дня, теряет сообщения первого дня в любом случае.
Можно ли использовать webhook на порту 3000 или 8080?
Напрямую нельзя. Telegram доставляет обновления только на 443, 80, 88 и 8443, а setWebhook отклоняет другие порты. Запускайте приложение на любом порту и поставьте перед ним обратный прокси на 443.
Следует ли Telegram за редиректами на мой webhook?
Считайте, что нет. Ответ 3xx записывается как неверный ответ, и доставка повторяется. Регистрируйте точный конечный URL, включая схему, хост и слеш в конце, если его ожидает ваш фреймворк.
Потеряю ли я сообщения из-за deleteWebhook?
Нет, если не передавать drop_pending_updates=true. Без этого обновления в очереди остаются на стороне Telegram и отдаются первому вызову getUpdates в пределах 24-часового лимита.
Нужен ли мне белый список IP, если я использую secret_token?
Нет. Секретный токен уже подтверждает, что запрос пришёл от Telegram, и не ломается, когда Telegram меняет диапазоны адресов. Белый список - дополнительный уровень, а не замена, и именно он чаще становится причиной сбоя.

