Лимиты Telegram Bot API: обработка 429 Retry-After, очереди и группировка сообщений

При масштабировании Telegram-бота или запуске массовых уведомлений разработчики неизбежно сталкиваются с жесткими лимитами Telegram Bot API. Игнорирование правил платформы приводит к ошибкам с кодом HTTP 429, временной блокировке отправки сообщений или полной заморозке работы бота во время пиковых нагрузок.

В этой статье мы разберем реальные лимиты сервера Telegram, построим отказоустойчивый HTTP-клиент на PHP для обработки ответов Retry-After, спроектируем систему очередей для исходящих сообщений и рассмотрим паттерны группировки данных, чтобы не превышать допустимую частоту вызовов.

Официальные и скрытые лимиты Telegram Bot API

Документация Telegram описывает основные лимиты, но на практике их поведение имеет нюансы, которые необходимо учитывать при проектировании архитектуры:

  • Общий лимит отправки: не более 30 сообщений в секунду суммарно по всем чатам для одного токена бота.
  • Лимит на личный чат: не более 1 сообщения в секунду в один и тот же приватный диалог с пользователем.
  • Группы и каналы: не более 20 сообщений в минуту в одну и ту же группу или канал.
  • Массовые рассылки: при отправке длинных цепочек уведомлений рекомендуемая скорость составляет не более 25–30 сообщений в секунду, даже если сообщения отправляются разным пользователям.
  • Payload webhook: размер тела входящего запроса от Telegram ограничен, а время ожидания ответа от вашего сервера (timeout) составляет 10–60 секунд. Если бэкенд не ответит кодом 200 OK вовремя, Telegram начнет повторно присылать те же самые update_id.

Если бот превышает допустимую частоту вызовов, API возвращает HTTP-статус 429 Too Many Requests с JSON-ответом, содержащим поле parameters.retry_after. Это значение указывает, сколько секунд нужно подождать перед выполнением следующего запроса.

Отказоустойчивый cURL-клиент с обработкой HTTP 429

Никогда не используйте file_get_contents для работы с Telegram API, так как эта функция не позволяет гибко обрабатывать заголовки HTTP, таймауты и коды ошибок уровня REST. Ниже представлен сервисный класс на PHP, реализующий правильную отправку через cURL с автоматическим перехватом 429 и повторной попыткой после паузы.

<?php

class TelegramApiClient
{
private string $token;
private int $maxRetries;

public function __construct(?string $token = null, int $maxRetries = 3)
{
$this->token = $token ?? (string) getenv('TELEGRAM_BOT_TOKEN');
$this->maxRetries = $maxRetries;

if (empty($this->token)) {
throw new InvalidArgumentException('Telegram bot token is missing.');
}
}

public function execute(string $method, array $params = []): array
{
$url = sprintf('https://api.telegram.org/bot%s/%s', $this->token, $method);
$attempt = 0;

while ($attempt < $this->maxRetries) {
$attempt++;

$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($params),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_CONNECTTIMEOUT => 5,
]);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);

if ($response === false) {
throw new RuntimeException('cURL error: ' . $curlError);
}

$decoded = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException('Invalid JSON response from Telegram API');
}

if ($httpCode === 429) {
$retryAfter = $decoded['parameters']['retry_after'] ?? 1;
sleep((int) $retryAfter);
continue;
}

if ($httpCode === 403) {
// Пользователь заблокировал бота или бот был удален из чата
return [
'ok' => false,
'error_code' => 403,
'description' => $decoded['description'] ?? 'Forbidden',
];
}

if (isset($decoded['ok']) && $decoded['ok'] === true) {
return $decoded;
}

return $decoded;
}

throw new RuntimeException('Exceeded maximum retry attempts for method: ' . $method);
}
}

Архитектура очереди исходящих сообщений (Rate Limiter)

Вызывать метод API напрямую из обработчика Webhook нельзя: при массовом притоке пользователей или выполнении рассылки веб-сервер исчерпает лимит соединений, а Telegram временно заблокирует отправку. Правильный подход — использовать асинхронную очередь задач (на базе Redis, RabbitMQ или таблицы в БД) с фоновым воркером.

Воркер должен контролировать две паузы: глобальный интервал (не более 30 запросов в секунду) и персональный интервал на чат (не менее 1.05 секунды между сообщениями одному пользователю).

<?php

class MessageQueueWorker
{
private TelegramApiClient $apiClient;
private PDO $db;

public function __construct(TelegramApiClient $apiClient, PDO $db)
{
$this->apiClient = $apiClient;
$this->db = $db;
}

public function processNext(): bool
{
// Выбираем из базы наиболее старое непрочитанное сообщение
$stmt = $this->db->prepare("
SELECT * FROM bot_outbox
WHERE status = 'pending' AND scheduled_at <= NOW()
ORDER BY id ASC LIMIT 1 FOR UPDATE SKIP LOCKED
");
$stmt->execute();
$job = $stmt->fetch(PDO::FETCH_ASSOC);

if (!$job) {
return false;
}

$chatId = $job['chat_id'];

// Проверяем время последней отправки в этот конкретный чат
$lastSentStmt = $this->db->prepare("SELECT last_sent_at FROM bot_chats WHERE chat_id = :chat_id");
$lastSentStmt->execute([':chat_id' => $chatId]);
$lastSentAt = $lastSentStmt->fetchColumn();

if ($lastSentAt) {
$timePassed = microtime(true) - (float) $lastSentAt;
if ($timePassed < 1.05) {
// Задерживаем обработку для соблюдения лимита 1 сообщ/сек на чат
usleep((int) ((1.05 - $timePassed) * 1000000));
}
}

$payload = json_decode($job['payload'], true);
$result = $this->apiClient->execute('sendMessage', $payload);

if (isset($result['ok']) && $result['ok'] === true) {
$this->markAsSent($job['id']);
$this->updateLastSentTime($chatId, microtime(true));
} else {
$errorCode = $result['error_code'] ?? 0;
if ($errorCode === 403) {
// Деактивируем пользователя, если он заблокировал бота
$this->markUserAsBlocked($chatId);
$this->markAsFailed($job['id'], 'User blocked bot');
} else {
$this->markAsFailed($job['id'], $result['description'] ?? 'Unknown error');
}
}

// Глобальный паузер между любыми исходящими запросами (максимум 30 TPS)
usleep(35000); // ~35 мс пауза = до 28 запросов в секунду

return true;
}

private function markAsSent(int $id): void
{
$stmt = $this->db->prepare("UPDATE bot_outbox SET status = 'sent', sent_at = NOW() WHERE id = :id");
$stmt->execute([':id' => $id]);
}

private function markAsFailed(int $id, string $reason): void
{
$stmt = $this->db->prepare("UPDATE bot_outbox SET status = 'failed', error_message = :err WHERE id = :id");
$stmt->execute([':err' => $reason, ':id' => $id]);
}

private function updateLastSentTime(int $chatId, float $microtime): void
{
$stmt = $this->db->prepare("
INSERT INTO bot_chats (chat_id, last_sent_at) VALUES (:chat_id, :last_sent)
ON DUPLICATE KEY UPDATE last_sent_at = :last_sent
");
$stmt->execute([':chat_id' => $chatId, ':last_sent' => $microtime]);
}

private function markUserAsBlocked(int $chatId): void
{
$stmt = $this->db->prepare("UPDATE bot_users SET is_active = 0 WHERE chat_id = :chat_id");
$stmt->execute([':chat_id' => $chatId]);
}
}

Группировка и батчинг сообщений в один чат

Распространенная ошибка — отправка нескольких связанных сообщений подряд (например, текстовый заголовок, затем фото, а следом блок с деталями заказа). Это тратит лимит 1 сообщ/сек и заставляет пользователя читать обрывки информации.

1. Объединение текста и клавиатуры

Формируйте единый HTML/Markdown-шаблон со всеми данными заказа и прикрепляйте к нему inline-клавиатуру за один вызов sendMessage. Максимальная длина текста сообщения в Telegram составляет 4096 символов, чего достаточно для большинства бизнес-сценариев.

2. Использование sendMediaGroup

Если необходимо отправить несколько фотографий или документов, группируйте их с помощью метода sendMediaGroup (от 2 до 10 файлов в одном альбоме). Это считается за 1 запрос к API Telegram.

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

Во время отправки уведомлений по крупной базе (от 10 000 адресатов и выше) вы обязательно столкнетесь со следующими ситуациями:

  1. Волна ошибок 403 Forbidden: часть пользователей удалит бота или заблокирует его. Если воркер продолжит шлобить API запросами к этим пользователям, платформа снизит общий рейтинг доверия к боту. При получении 403 отключайте отправку для данного chat_id в БД мгновенно.
  2. Каскадный 429: если воркер получает 429 и в вашей системе запущено несколько параллельных процессов отправки, они все начнут получать 429 одновременно. Используйте общую паузу в Redis (Rate Limiter Circuit Breaker), чтобы при возникновении 429 в одном воркере все остальные процессы замирали на указанный паузой retry_after интервал.
  3. Забивание вебхука во время рассылки: отправка не должна блокировать синхронную обработку входящих команд пользователей. Разделяйте очереди: создавайте высокий приоритет для ответов на входящие сообщения (например, нажатие кнопки) и низкий приоритет для фоновых рассылок.

Разработка отказоустойчивых Telegram-ботов и интеграций любой сложности — BotCreator.

Новые статьи — в Telegram

Разбираем, что автоматизировать в бизнесе и как это работает на практике. Без спама.