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

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

Жесткие лимиты Telegram Bot API: цифры и правила

Официальная документация Telegram устанавливает несколько уровней ограничений на количество исходящих запросов:

  • Глобальный лимит бота: не более 30 сообщений в секунду суммарно по всем чатам.
  • Лимит на один личный чат: не более 1 сообщения в секунду. Допускаются короткие всплески, но при длительной отправке бот моментально получает HTTP 429.
  • Лимит на группы и каналы: не более 20 сообщений в минуту.
  • Метод sendMediaGroup: передача альбома из нескольких файлов считается за один запрос к API, но регулируется теми же лимитами по чатам.

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

Обработка ошибки 429 Retry-After на чистом PHP через cURL

Для надежного взаимодействия с api.telegram.org нельзя использовать базовую функцию file_get_contents, так как она не позволяет гибко читать заголовки ответов и тела ошибок при HTTP-статусах, отличных от 200. Валидный HTTP-клиент должен проверять сетевые ошибки, распарсивать JSON и корректно извлекать время ожидания.

<?php

function sendTelegramMessage(string $method, array $params): array
{
$token = getenv('TELEGRAM_BOT_TOKEN');
if (!$token) {
throw new \RuntimeException('TELEGRAM_BOT_TOKEN environment variable is not set');
}

$url = "https://api.telegram.org/bot{$token}/{$method}";
$ch = curl_init($url);

curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($params),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
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) {
return [
'ok' => false,
'error_code' => 500,
'description' => 'cURL error: ' . $curlError
];
}

$data = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return [
'ok' => false,
'error_code' => 500,
'description' => 'Invalid JSON response from Telegram API'
];
}

if ($httpCode === 429) {
$retryAfter = $data['parameters']['retry_after'] ?? 3;
return [
'ok' => false,
'error_code' => 429,
'retry_after' => (int)$retryAfter,
'description' => $data['description'] ?? 'Too Many Requests'
];
}

return $data;
}

Архитектура исходящей очереди в Laravel с поддержкой Retry-After

Отправка массовых рассылок или транзакционных уведомлений напрямую из веб-запроса неэффективна. Все исходящие сообщения должны отправляться через фоновые очереди (Laravel Queue, RabbitMQ или Redis).

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

<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class SendTelegramNotificationJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

public int $tries = 5;
public int $maxExceptions = 3;

public function __construct(
public int $chatId,
public string $text,
public string $leadId
) {}

public function handle(): void
{
$token = config('services.telegram.bot_token');

$response = Http::timeout(10)
->acceptJson()
->post("https://api.telegram.org/bot{$token}/sendMessage", [
'chat_id' => $this->chatId,
'text' => $this->text,
'parse_mode' => 'HTML',
'disable_web_page_preview' => true,
]);

if ($response->status() === 429) {
$retryAfter = (int) $response->json('parameters.retry_after', 5);
Log::warning("Telegram limit reached. Retrying job for lead {$this->leadId} after {$retryAfter}s.");

// Освобождаем задачу обратно в очередь с паузой
$this->release($retryAfter + 1);
return;
}

$responseData = $response->json();

if (!$response->successful() || !($responseData['ok'] ?? false)) {
$description = $responseData['description'] ?? 'Unknown API Error';

// Если пользователь заблокировал бота, повторы бессмысленны
if ($response->status() === 403) {
Log::notice("Bot was blocked by user {$this->chatId} for lead {$this->leadId}.");
return;
}

Log::error("Failed to send Telegram message to {$this->chatId}: {$description}");
$this->fail(new \RuntimeException("Telegram API Error: {$description}"));
}
}
}

Группировка уведомлений: стратегия снижения нагрузки на API

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

Правильный подход — буферизация и объединение нескольких текстовых фрагментов в одно итоговое сообщение. Telegram поддерживает до 4096 символов в одном вызове sendMessage.

<?php

class TelegramMessageBuffer
{
private array $buffer = [];

public function addNotification(int $chatId, string $line): void
{
$this->buffer[$chatId][] = $line;
}

public function flush(callable $sendCallback): void
{
foreach ($this->buffer as $chatId => $lines) {
$currentChunk = '';

foreach ($lines as $line) {
// Превышение лимита символов в одном сообщении (оставляем запас от 4096)
if (mb_strlen($currentChunk . "
" . $line) > 4000) {
$sendCallback($chatId, trim($currentChunk));
$currentChunk = $line;
usleep(100000); // Задержка 100мс для соблюдения лимитов чата
} else {
$currentChunk .= ($currentChunk === '' ? '' : "

") . $line;
}
}

if ($currentChunk !== '') {
$sendCallback($chatId, trim($currentChunk));
usleep(100000);
}
}

$this->buffer = [];
}
}

// Пример использования буфера
$buffer = new TelegramMessageBuffer();

// Заполняем событиями по лиду
$leadId = bin2hex(random_bytes(7));
$buffer->addNotification(123456789, "<b>Заявка #{$leadId}</b> принята в обработку.");
$buffer->addNotification(123456789, "Назначен менеджер: Иван.");
$buffer->addNotification(123456789, "Статус изменен на: <i>Ожидает оплаты</i>.");

// Сбрасываем накопленные сообщения единым отправлением
$buffer->flush(function (int $chatId, string $text) use ($leadId) {
// В реальности здесь вызывается задача очереди или cURL-клиент
sendTelegramMessage('sendMessage', [
'chat_id' => $chatId,
'text' => $text,
'parse_mode' => 'HTML',
]);
});

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

При проведении рассылок по базе в десятки тысяч пользователей стандартный скрипт сталкивается с рядом типовых проблем:

  1. Падение воркеров по памяти: при выборке всей базы подписчиков в один массив скрипт быстро исчерпывает memory_limit. Выборку необходимо делать через чанки (chunk() в Laravel или курсоры в PDO).
  2. Обработка 403 Forbidden: если пользователь заблокировал бота, Telegram возвращает статус 403. Скрипт рассылки обязан помечать таких пользователей в БД (например, ставить is_active = false), чтобы не тратить на них лимиты при последующих запусках.
  3. Отсутствие идемпотентности: при сбое воркера очередь может повторно запустить отправку той же порции сообщений. Каждому уведомлению должен присваиваться уникальный идентификатор (например, lead_id или UUID отправки), который фиксируется в БД перед запросом к API.
При рассылках всегда настраивайте глобальное ограничение скорости (Rate Limiting) на стороне очереди. Для Redis в Laravel оптимальной настройкой будет Redis::throttle('telegram-broadcast')->allow(25)->every(1), что позволит запасти 5 запросов в секунду для приоритетных транзакционных сообщений.

Если вам нужна разработка отказоустойчивых ботов и интеграций под ключ, обратитесь к команде BotCreator.

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

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