MAATRIX / Блог / Предел одного Telegram-бота: сколько сообщений в секунду до первых ошибок 429

Предел одного Telegram-бота: сколько сообщений в секунду до первых ошибок 429

MAATRIX

Бот работал нормально, пока аудитория не выросла до нескольких тысяч подписчиков и не понадобилась массовая рассылка. Первый же прогон «отправить всем» кладёт цикл на землю: часть сообщений уходит, часть возвращается с ошибкой 429 Too Many Requests, часть теряется, если исключение не поймано. Проблема не в вашем коде как таковом и не в хостинге — Telegram Bot API просто ограничивает скорость приёма сообщений, и любой бот, который отправляет быстрее этого предела, рано или поздно в него упрётся. Разберём, где именно проходит граница, что означает ответ 429 и как построить очередь отправки, которая не роняет рассылку и не требует героических костылей в проде.

Какие лимиты у Telegram Bot API на самом деле

Telegram не публикует эти цифры как формальный контракт API с гарантией на все времена — это скорее общеизвестные ориентиры, которые сообщество ботоводов подтверждает годами практики и которые встречаются в официальной документации как рекомендации. Держите в уме, что это именно ориентиры, а не жёстко задокументированная спецификация, и что на конкретном аккаунте бота цифры могут отличаться:

  • около 30 сообщений в секунду — суммарный поток бота в разные чаты (broadcast-лимит);
  • около 1 сообщения в секунду в один и тот же чат или группу — если долбить одного пользователя чаще, начнутся 429;
  • для групп и супергрупп порог ощутимо ниже, чем для личных чатов — на практике безопаснее ориентироваться на единицы сообщений в минуту в одну группу, если это не критично важное уведомление;
  • лимиты считаются как минимум на уровне одного бота (по токену), поэтому несколько независимых ботов с разными токенами имеют независимые бюджеты — это отдельная тема, разобранная в статье про архитектуру нескольких ботов на одном сервере.

Важный нюанс: лимит «в разные чаты» и лимит «в один чат» — это два разных счётчика, и превысить можно любой из них независимо. Бот, рассылающий 20 сообщений в секунду в 20 разных личных чатов, скорее всего не получит 429 по общему лимиту. Тот же бот, пытающийся отправить 20 сообщений в секунду в одну группу поддержки, получит 429 почти сразу — там сработает лимит на чат, а не общий.

Отдельно стоят медиа-группы (альбомы) и редактирование сообщений — у них исторически более строгие лимиты, чем у обычного sendMessage, единый бюджет на все типы запросов закладывать не стоит.

Что означает ошибка 429 и поле retry_after

Когда бот превышает лимит, Telegram Bot API не отклоняет запрос молча и не банит бота — он возвращает HTTP-ответ с кодом 429 Too Many Requests и телом вида:

{
  "ok": false,
  "error_code": 429,
  "description": "Too Many Requests: retry after 3",
  "parameters": {
    "retry_after": 3
  }
}

Поле retry_after — это число секунд, через которое сервер рекомендует повторить попытку. Это не оценка «примерно скоро», а конкретное значение, посчитанное сервером под конкретный чат или под конкретного бота в данный момент — и именно на него нужно ориентироваться, а не на собственный фиксированный таймаут.

Ключевая ошибка многих реализаций — воспринимать 429 как обычную сетевую ошибку и обрабатывать её общим try/except с логированием и пропуском сообщения. Это не так: 429 — не сбой доставки, а сигнал «подождите и повторите», и сообщение при правильной обработке всё равно должно быть доставлено, просто с задержкой. Если игнорировать retry_after и повторять запрос сразу — велик шанс получить следующий 429 с ещё большим значением задержки, потому что бот продолжает давить на тот же лимит.

Второй частый источник путаницы — смешивание 429 с другими кодами ошибок. 403 Forbidden (бот заблокирован пользователем или кикнут из группы) и 400 Bad Request (например, чат не найден) — это постоянные ошибки, повторять отправку для них бессмысленно, сообщение нужно просто списать и убрать получателя из базы. 429 — единственная из типичных ошибок API, где повтор не только оправдан, но и ожидается сервером.

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

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

Арендовать сервер

Очередь отправки и правильный backoff

Прямой цикл for user in users: bot.send_message(...) работает до первых сотен получателей и разваливается на тысячах. Правильная схема — очередь, через которую все исходящие сообщения проходят с контролируемым темпом, а не разлетаются параллельно из разных мест кода.

Минимальная реализация на Python с asyncio и токен-бакетом на глобальный лимит:

import asyncio
import time

class RateLimiter:
    def __init__(self, rate: float, per: float = 1.0):
        self.rate = rate          # сколько токенов
        self.per = per            # за какой период, сек
        self.tokens = rate
        self.updated = time.monotonic()
        self.lock = asyncio.Lock()

    async def acquire(self):
        async with self.lock:
            now = time.monotonic()
            elapsed = now - self.updated
            self.tokens = min(self.rate, self.tokens + elapsed * (self.rate / self.per))
            self.updated = now
            if self.tokens < 1:
                wait = (1 - self.tokens) * (self.per / self.rate)
                await asyncio.sleep(wait)
                self.tokens = 0
            else:
                self.tokens -= 1

global_limiter = RateLimiter(rate=25, per=1.0)  # с запасом от ~30/сек

Лимит взят с запасом (25 вместо ориентировочных 30) — троттлинг на своей стороне стоит делать немного строже общего ориентира, потому что сетевые задержки и параллельные запросы всё равно создают всплески.

Отправка с обработкой 429 и уважением retry_after:

async def send_with_retry(bot, chat_id, text, max_retries=5):
    for attempt in range(max_retries):
        await global_limiter.acquire()
        try:
            return await bot.send_message(chat_id, text)
        except TelegramRetryAfter as e:
            # библиотека (aiogram, python-telegram-bot) обычно
            # парсит retry_after сама и кладёт его в исключение
            await asyncio.sleep(e.retry_after + 0.5)
        except TelegramForbiddenError:
            # пользователь заблокировал бота — не повторяем
            mark_user_unreachable(chat_id)
            return None
    log.warning("send failed after retries: %s", chat_id)
    return None

Задержка после 429 берётся из retry_after, а не из своей формулы экспоненты — сервер знает точнее, сколько ждать. К паузе добавлен небольшой запас (+0.5), потому что момент снятия ограничения на сервере и момент, когда до него долетит ваш запрос, не совпадают идеально.

Классический экспоненциальный backoff (без retry_after, если библиотека его не отдаёт или ошибка не 429, а сетевая — таймаут, разрыв соединения) выглядит иначе:

async def send_with_backoff(bot, chat_id, text, max_retries=5):
    delay = 1.0
    for attempt in range(max_retries):
        try:
            return await bot.send_message(chat_id, text)
        except (TimeoutError, ConnectionError):
            await asyncio.sleep(delay + random.uniform(0, delay * 0.3))  # джиттер
            delay = min(delay * 2, 30)
    return None

Джиттер (случайная добавка к паузе) важен, если у вас параллельно работает несколько воркеров: без него все воркеры после сбоя проснутся синхронно и создадут новый всплеск нагрузки — тот самый эффект thundering herd. Общий принцип троттлинга подробнее разобран в статье как работает rate limiting — там же объяснена разница между токен-бакетом и скользящим окном, если нужно выбрать алгоритм под конкретную задачу, а не только под Telegram.

Архитектура рассылки на большую аудиторию: батчинг и троттлинг

Когда аудитория растёт до десятков и сотен тысяч подписчиков, простого асинхронного цикла с лимитером уже недостаточно — рассылка должна быть отдельным управляемым процессом, а не побочным эффектом обработчика команды /broadcast. Практическая схема, которая устойчиво работает на одном VPS:

  1. Очередь задач — Redis (список или Stream) или простая таблица в PostgreSQL со статусами pending / sent / failed. Список получателей формируется один раз при старте рассылки, а не собирается на лету.
  2. Пул воркеров с общим лимитером — несколько асинхронных задач читают из очереди, но все проходят через один и тот же RateLimiter (как в примере выше), а не имеют собственный независимый темп — иначе суммарная скорость легко превысит общий лимит бота, даже если каждый воркер по отдельности «медленный».
  3. Батчинг по времени, а не по объёму — рассылка растягивается во времени, а не отправляется «пачкой» разом: при лимите около 30 сообщений в секунду 100 000 получателей — это около часа непрерывной отправки, и это нормально, лимит всё равно не даст ускориться.
  4. Разделение по типам получателей — если часть получателей это группы, а часть — личные чаты, стоит вести для них отдельные очереди с разным темпом, потому что лимит на чат для групп жёстче.
  5. Персистентность прогресса — при перезапуске бота (деплой, падение процесса) рассылка должна продолжаться с того места, где остановилась, а не с начала: статус sent в базе — это дешёвая защита от повторной отправки одного и того же сообщения тысячам людей.

Ориентировочная таблица зависимости времени рассылки от размера аудитории при консервативном темпе около 25 сообщений в секунду:

ПолучателейВремя рассылки (ориентировочно)
1 000~40 секунд
10 000~7 минут
100 000~1 час 7 минут
1 000 000~11 часов

Это именно ориентир при равномерном темпе без сбоев и без повторов — на практике добавляются паузы на 429 у отдельных чатов, повторные попытки и обработка ошибок, так что закладывайте запас 15–25% сверху. Как устроена сама рассылка по подписчикам на уровне кода бота, включая хранение списка получателей и обработку отписок, подробно разобрано в статье про бота для рассылки по подписчикам.

Для по-настоящему больших аудиторий (от миллиона и выше) имеет смысл рассмотреть несколько ботов с разными токенами, каждый со своим бюджетом лимита, и распределять получателей между ними — но это усложняет инфраструктуру (нужно, чтобы пользователь взаимодействовал именно с «его» ботом) и оправдано не всегда: сначала стоит убедиться, что узкое место — действительно лимит API, а не, например, сеть или диск на сервере.

Мониторинг и деградация под нагрузкой

Рассылка на большую аудиторию — это часы непрерывной работы, и за это время что-то обязательно пойдёт не так: истечёт токен, оборвётся сеть, упадёт Redis. Без наблюдаемости узнать об этом получится только когда пользователи начнут жаловаться, что рассылка «зависла». Стоит логировать и по возможности выводить в дашборд:

  • текущую скорость отправки — если она заметно ниже настроенного лимита, очередь пуста или воркеры застряли на повторах;
  • количество 429 за последнюю минуту — рост метрики означает, что реальный лимит ниже заложенного, темп нужно снизить;
  • количество 403 Forbidden — это не троттлинг, а отток аудитории (блокировки бота), стоит считать отдельно;
  • глубину очереди и оценку оставшегося времени — чтобы понимать, когда рассылка реально закончится.

Полезная практика — алерт в отдельный служебный чат при аномальном росте 429 или при полной остановке отправки дольше нескольких минут; как быстро поднять такие уведомления на сервере, описано в статье про алерты в Telegram на VPS. Заметьте: этот алерт должен идти через отдельного бота или отдельный высокоприоритетный канал очереди — если пускать его через тот же лимитированный поток, что и саму рассылку, уведомление о проблеме может застрять в той же очереди, что и проблема.

Частые ошибки при массовых рассылках

  • Параллельные воркеры без общего лимитера. Каждый воркер сам по себе укладывается в 10 сообщений в секунду, но их пять — и суммарно бот отправляет 50 в секунду, вдвое больше ориентировочного предела. Лимитер должен быть один и общий на процесс (или общий через Redis, если воркеров несколько на разных серверах).
  • Игнорирование retry_after в пользу своей формулы задержки. Собственный экспоненциальный backoff хорош для сетевых сбоев, но для 429 сервер уже посчитал нужную паузу — использовать вместо неё фиксированную секунду или произвольную экспоненту означает либо ждать дольше необходимого, либо снова упереться в лимит раньше времени.
  • Блокирующие вызовы внутри асинхронного воркера. Синхронный HTTP-клиент или тяжёлая операция с базой без await внутри цикла отправки останавливает весь event loop — темп отправки скачет не из-за лимитов Telegram, а из-за собственной архитектуры бота.
  • Отсутствие различия между личными чатами и группами. Код, который одинаково гонит сообщения в группы и в личку с одним лимитом, будет ловить 429 в группах гораздо чаще — там порог ниже.
  • Потеря прогресса при перезапуске. Без сохранения статуса sent/pending в базе рестарт бота во время рассылки означает либо повторную отправку части аудитории, либо потерю части получателей — оба варианта плохо смотрятся в проде.
  • Отправка без учёта отписок и блокировок. Если не обрабатывать 403 Forbidden и не убирать таких пользователей из базы, каждая следующая рассылка тратит время и один и тот же слот лимита на заведомо недоставимые сообщения — на большой базе это ощутимо снижает эффективную скорость полезной рассылки.
  • Тестирование на маленькой аудитории и экстраполяция «в лоб». Рассылка на 50 человек пройдёт мгновенно и не покажет проблем с троттлингом — они проявляются только на объёмах, где лимит реально становится узким местом, поэтому нагрузочный прогон стоит делать хотя бы на нескольких тысячах тестовых chat_id или с намеренно заниженным лимитом для проверки логики повторов.

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

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

Арендовать сервер

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

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

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

Можно ли обойти лимит, увеличив число серверов бота?

Нет — лимит привязан к токену бота на стороне Telegram, а не к серверу или IP-адресу, с которого идут запросы. Несколько серверов помогут распределить вычислительную нагрузку, но не увеличат бюджет сообщений в секунду для одного бота.

Что будет, если совсем игнорировать 429 и просто повторять запрос сразу?

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

Нужен ли Redis для очереди, если аудитория небольшая (пара тысяч подписчиков)?

Не обязательно — для такого объёма достаточно очереди в памяти процесса (asyncio.Queue) с одним общим лимитером, а прогресс можно сохранять прямо в таблицу базы данных, которая у бота и так есть. Redis имеет смысл, когда воркеров несколько или процесс бота может перезапускаться посреди рассылки.

Как понять, что узкое место — именно лимит Telegram, а не сеть или сервер?

Смотрите на код ответа: если это HTTP 429 именно от api.telegram.org с полем retry_after — это лимит API. Таймауты, разрывы соединения или медленный ответ без кода 429 — это, скорее всего, сетевые проблемы или нагрузка на сам сервер бота, и лечится это по-другому: проверкой пропускной способности сети и ресурсов VPS.

Стоит ли слать одно и то же сообщение через sendMediaGroup вместо нескольких sendMessage, чтобы сэкономить лимит?

Нет, это разные механизмы: sendMediaGroup считается отдельным методом со своими, обычно более строгими ограничениями, и не предназначен для экономии общего лимита сообщений — использовать его стоит только когда действительно нужен альбом из нескольких медиафайлов в одном сообщении.

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

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

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