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