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