MAATRIX / Блог / Telegram-бот: webhook или polling — что выгоднее и когда

Telegram-бот: webhook или polling — что выгоднее и когда

Telegram-бот: webhook или polling — что выгоднее и когда

MAATRIX

Выбор «telegram бот webhook или polling» решают по слухам: «polling тормозит на тридцать секунд», «webhook сложный и дорогой». Оба утверждения неверны — разница в задержке измеряется десятками миллисекунд, а граница проходит по другим линиям: белый IP, порядок апдейтов, возможность запустить второй инстанс. Ниже замеры и команды для перехода в обе стороны без потери очереди.

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

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

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

Как оба режима работают на самом деле

Миф про polling вырос из путаницы между обычным и длинным опросом. Бот вызывает getUpdates с параметром timeout, и Telegram не ждёт эти секунды: он держит соединение открытым и отвечает в момент появления апдейта. Таймаут — потолок ожидания, а не задержка:

TOKEN=123456:AA...
curl -s "https://api.telegram.org/bot$TOKEN/getUpdates?timeout=50&limit=100" | jq '.result | length'

Напишите боту — ответ вернётся мгновенно; если апдейтов нет, через 50 секунд придёт {"ok":true,"result":[]}.

Второй механизм polling — подтверждение через offset: пока вы не вызвали getUpdates со значением offset = max(update_id) + 1, Telegram считает пачку неподтверждённой и отдаст её снова. Отсюда два следствия: апдейты приходят строго последовательно по возрастанию update_id, а падение процесса до обработки означает повторную доставку, а не потерю.

Webhook зеркален: вы отдаёте Telegram HTTPS-URL, он сам шлёт POST с телом одного апдейта, параллельно — до max_connections штук (по умолчанию 40, диапазон 1–100). Подтверждение здесь код 200; любой другой ответ или таймаут означает повторную доставку. Общее у обоих режимов: доставка как минимум один раз, очередь живёт 24 часа, фильтр allowed_updates работает одинаково.

Замеры: задержка, трафик, память

Цифры сняты на одной машине — VPS в Лондоне, 2 vCPU / 4 ГБ, Ubuntu 24.04, Python 3.12, aiogram 3.x. Методика: скрипт слал боту /ping и засекал монотонное время до отправки и после ответа, 500 итераций. Замер включает обе ноги маршрута, так что интересна разница между режимами, а не абсолютная цифра.

МетрикаLong polling (timeout=50)Webhook (nginx + aiohttp)
Медиана round-trip /ping0,31 с0,24 с
95-й перцентиль0,52 с0,38 с
Запросов наружу в простое~1,2/мин, около 52 000/мес0
Исходящий трафик в простое40–50 МБ/мес~0, плюс ACME и сканеры
CPU процесса в простое0,3–0,5% одного vCPU0,0%
RSS процесса58 МБ61 МБ + nginx ~15 МБ

Свои цифры снимите так же: vnstat -i eth0 -d даст около 1,5–1,7 МБ в сутки на пустом polling-боте, pidstat -p $(pgrep -f bot.py) 10 3 — загрузку CPU.

Вывод честный и скучный: разница в задержке порядка 70 мс, пользователь её не заметит. Значение она приобретает, только если один апдейт запускает цепочку из пяти-шести вызовов Bot API.

Ещё одна цифра ломает довод «polling не держит нагрузку»: getUpdates отдаёт до 100 апдейтов за вызов, и при RTT в 30–40 мс это потолок около 2500 апдейтов в секунду на одном соединении. Пропускная способность кончается позже, чем принято думать; кончается другое — возможность масштабироваться вширь.

Развернуть за пару минут

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

Развернуть бот-стек

Где polling выигрывает

Нет белого IP и домена. Сервер за NAT, домашний мини-ПК, чужой контейнер — polling работает везде, где есть исходящий HTTPS. Вебхуку здесь нужен туннель (Cloudflare Tunnel, ngrok, обратный SSH): ещё один сервис, который может упасть, плюс 20–60 мс задержки.

Порядок апдейтов важен. Polling отдаёт события строго последовательно. Для пошаговой анкеты или платёжного сценария, где «сначала А, потом Б», это требование, а не роскошь; webhook такого не гарантирует.

Минимальная поверхность атаки и простая отладка. Наружу не торчит ничего: sudo ufw status numbered показывает единственное правило 22/tcp ALLOW IN. А на ноутбуке достаточно python bot.py — без ngrok и без переустановки вебхука после каждого перезапуска.

Честный минус ровно один, но крупный: один токен — один опрашивающий процесс. Второй экземпляр получит Conflict: terminated by other getUpdates request; make sure that only one bot instance is running. Значит, ни масштабирования вширь, ни blue-green деплоя: при обновлении бот на секунду-две молчит.

Где webhook выигрывает

Несколько инстансов за одним токеном. Главный и, по сути, единственный незаменимый аргумент: nginx раскидывает апдейты по нескольким процессам, и бот перестаёт быть единой точкой отказа.

upstream tgbot {
    least_conn;
    server 127.0.0.1:8081 max_fails=2 fail_timeout=5s;
    server 127.0.0.1:8082 max_fails=2 fail_timeout=5s;
}

server {
    listen 443 ssl http2;
    server_name bot.example.com;
    ssl_certificate     /etc/letsencrypt/live/bot.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/bot.example.com/privkey.pem;

    location /tg/8f21b0c4d9 {
        proxy_pass http://tgbot;
        proxy_read_timeout 15s;
        proxy_next_upstream error timeout http_502 http_504;
    }
    location / { return 404; }
}

Грабля с синтаксисом: в nginx из репозитория Ubuntu 24.04 (1.24.0) HTTP/2 включается как listen 443 ssl http2;, а с 1.25.1 эта форма устарела в пользу директивы http2 on; — скопировав старый конфиг в свежий nginx, получите в nginx -t предупреждение the "listen ... http2" directive is deprecated. Впрочем, для вебхука это косметика: Telegram ходит по HTTP/1.1.

Деплой без простоя. Поднимаете процесс на 8083, добавляете в upstream, sudo nginx -s reload, убираете старый — апдейты не теряются. С polling так нельзя.

Экономия целого round-trip. Bot API позволяет вернуть вызов метода прямо в теле ответа на вебхук: вместо пустого 200 OK вы отдаёте JSON с полем method, и Telegram выполняет его сам, без запроса к api.telegram.org. Честное ограничение: результат вызова и его ошибку вы не увидите.

Скрытые риски вебхука, о которых узнают в проде

Сертификат — самая частая причина тихой смерти бота. Let's Encrypt живёт 90 дней; таймер продления упал или закрыт 80-й порт — и на 91-й день апдейты перестают приходить, при этом процесс жив, systemctl status зелёный, в логах пусто. Проверьте systemctl list-timers 'certbot*' и certbot renew --dry-run, а в cron поставьте внешний контроль вебхука:

curl -s "https://api.telegram.org/bot$TOKEN/getWebhookInfo" \
  | jq -r '.result | "\(.pending_update_count) \(.last_error_message // "ok")"'

Строка вида 213 Wrong response from the webhook: 502 Bad Gateway или просто растущее первое число — сигнал, что очередь копится и через сутки пропадёт.

Дубли. Любой ответ кроме 200 и любой таймаут заставляют Telegram повторить апдейт: если обработчик успел списать баланс и упал на отправке сообщения, при повторе он спишет ещё раз. Лечится идемпотентностью по update_id:

if not await redis.set(f"tg:upd:{update.update_id}", 1, nx=True, ex=86400):
    return  # уже обработан

Нет гарантии порядка. До 40 параллельных соединений означают, что два быстрых нажатия inline-кнопки прилетят в разные воркеры одновременно: пользователь дважды тапнул «Оплатить», обе транзакции прочитали один баланс. Лечится блокировкой на чат — SET lock:chat:{chat_id} 1 NX PX 5000 в Redis.

Открытый 443 наружу. Через сутки в access.log появятся запросы к /.env и /wp-login.php. Telegram документирует свои подсети — вебхук закрывается только для них:

sudo ufw allow from 149.154.160.0/20 to any port 443 proto tcp
sudo ufw allow from 91.108.4.0/22 to any port 443 proto tcp
sudo ufw delete allow 443/tcp

Две оговорки: если на сервере крутится сайт, вы отрежете и его посетителей; а 80-й порт надо оставить открытым, иначе сломается проверка http-01 при продлении сертификата. Итого: у polling в цепочке один компонент, у вебхука пять — DNS, сертификат, nginx, приложение, systemd (разбор ошибок каждого).

Как переключиться в обе стороны без потери апдейтов

Два режима одновременно не живут, порядок команд важен. Запустите polling при активном вебхуке — получите ровно такой ответ:

{"ok":false,"error_code":409,"description":"Conflict: can't use getUpdates method while webhook is active; use deleteWebhook to delete the webhook first"}

Polling → webhook. Сначала гасим опрашивающий процесс, потом ставим вебхук.

sudo systemctl stop tgbot
curl -s -X POST "https://api.telegram.org/bot$TOKEN/setWebhook" \
  -d "url=https://bot.example.com/tg/8f21b0c4d9" \
  -d "secret_token=$WH_SECRET" \
  -d 'allowed_updates=["message","callback_query","my_chat_member"]' \
  -d "max_connections=20"
sudo systemctl start tgbot-web

Успех — {"ok":true,"result":true,"description":"Webhook was set"}. Случайный сегмент в пути и secret_token не паранойя: без них эндпоинт дёрнет любой прохожий.

Webhook → polling. Зеркально: гасим tgbot-web, снимаем вебхук, поднимаем tgbot.

curl -s "https://api.telegram.org/bot$TOKEN/deleteWebhook?drop_pending_updates=false"

Значение false сохранит очередь, бот доразберёт её сам; true ставьте, только если переезжаете после долгого простоя и не хотите, чтобы бот разом ответил на две тысячи вчерашних сообщений.

Две ветки кода держать не надо — режимы сводят в один файл и переключают переменной окружения:

from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application

if os.getenv("BOT_MODE") == "webhook":
    app = web.Application()
    SimpleRequestHandler(dispatcher=dp, bot=bot,
                         secret_token=os.environ["WH_SECRET"]).register(app, path=WH_PATH)
    setup_application(app, dp, bot=bot)
    # только 127.0.0.1: на 0.0.0.0 приложение торчит наружу мимо nginx
    web.run_app(app, host="127.0.0.1", port=int(os.getenv("PORT", 8081)))
else:
    asyncio.run(dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types()))

Бинд проверяется командой ss -lntp | grep 8081: там должен быть локальный адрес.

Чек-лист после переключения: в getWebhookInfo нужный url и пустой last_error_message, pending_update_count за минуту вернулся к нулю, бот отвечает на /start, после sudo reboot поднялся сам. Последний пункт проваливают чаще всего — разбор в статье почему бот падает после перезапуска.

Какой сервер брать в MAATRIX под каждый режим

Требования скромные, но граница «хватит / не хватит» проходит именно по режиму.

СценарийКонфигурацияЧто получаете
Polling, SQLite, до тысячи пользователей1 vCPU, 1 ГБ RAM, 20 ГБ NVMeОдин процесс, наружу ничего
Webhook + nginx + PostgreSQL + Redis2 vCPU, 4 ГБ RAM, 40–60 ГБ NVMeЧестный минимум для прода
Webhook, 2–4 инстанса, деплой без простоя4 vCPU, 8 ГБ RAM, 80 ГБ NVMeПики, рассылки, воркеры

Честно про минимум: 1 ГБ живёт ровно до момента, когда рядом появляются nginx, PostgreSQL и Redis — вместе они съедают 400–600 МБ, и первым под OOM-killer уходит процесс с самым большим RSS, то есть ваш Python. Проверяется строкой journalctl -k | grep -i "killed process": есть совпадение — вопрос не к коду, а к тарифу (расчёт памяти). Комфортный вариант под вебхук — 2 vCPU и 4 ГБ: второе ядро нужно не боту, а nginx и базе.

Проговорите при заказе: вебхуку обязателен выделенный белый IPv4 с портами 80 и 443 — за общим NAT он не работает вовсе.

Локацию берите в Лондоне, и по неочевидной причине: пинг между вами и пользователем не значит ничего — он общается с Telegram, а не с вашей машиной. Важен участок «сервер ↔ Bot API», он же определяет, как быстро Telegram достучится до вебхука. С лондонской площадки curl -o /dev/null -s -w 'connect %{time_connect} total %{time_total}\n' https://api.telegram.org даёт порядка 0,008 и 0,03 секунды. Франция не хуже, если аудитория в ЕС; Россия — когда бот собирает персданные россиян и 152-ФЗ важнее пары десятков миллисекунд; США — когда бот ходит в OpenAI или Claude и нужен чистый IP.

Оплата — картами российских банков, по СБП, криптой или токеном MAAT: зарубежная карта не нужна. Развёртывание с нуля — в инструкции как поднять Telegram-бота на Python на VPS.

Развернуть за пару минут

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

Развернуть бот-стек

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

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

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

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

Правда ли, что polling добавляет задержку в 30 секунд?

Нет. timeout в getUpdates — максимальное время ожидания пустого ответа, а не задержка доставки: как только апдейт появился, Telegram отвечает немедленно. На замерах разница с вебхуком около 70 мс.

Можно ли держать webhook и polling одновременно, для подстраховки?

Нельзя: пока вебхук установлен, getUpdates возвращает 409 с текстом can't use getUpdates method while webhook is active. Сначала deleteWebhook, потом polling.

Обязателен ли домен для вебхука?

В штатном варианте да: нужен HTTPS-URL с сертификатом, который Telegram проверит. Обойти можно, загрузив самоподписанный сертификат параметром certificate в setWebhook и указав адрес вида https://203.0.113.10:8443/..., но продлевать его придётся руками.

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

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