Разработчики Telegram-ботов часто сталкиваются с ситуацией, когда при росте базы пользователей система начинает давать сбои. Бот внезапно перестаёт отвечать, в логах появляются ошибки 429 Too Many Requests, а новые входящие Webhook-запросы образуют гигантскую очередь. Причина кроется в жестких лимитах Telegram Bot API, которые требуют грамотного архитектурного проектирования отправки сообщений.
В этой статье мы подробно разберем официальные и скрытые лимиты платформы, научимся корректно перехватывать заголовок retry_after через cURL, организуем безопасную очередь отправки в Yii2 и рассмотрим паттерн группировки (агрегации) уведомлений для одного чата.
Официальные и фактические лимиты Telegram Bot API
Telegram накладывает несколько базовых ограничений на отправку исходящих сообщений. Если их превысить, сервер API возвращает код HTTP 429 с телом JSON, содержащим поле parameters.retry_after.
- Глобальный лимит на бота: не более 30 сообщений в секунду для всех чатов суммарно.
- Лимит на конкретный приватный чат: не более 1 сообщения в секунду. В редких случаях допустимы короткие всплески, но длительная отправка с интервалом менее 1000 мс приводит к блокировке.
- Лимит на группы и каналы: не более 20 сообщений в минуту в одну группу/канал.
- Максимальный размер текста: 4096 символов для текстового сообщения (и 1024 символа для подписей к медиафайлам).
- Размер payload в callback_data: не более 64 байт.
Главная ошибка начинающих разработчиков — попытка отправлять исходящие сообщения в синхронном режиме сразу во время обработки входящего Webhook-запроса или по прямому циклу foreach из базы данных. Это мгновенно приводит к каскадному падению приложения.
Парсинг 429 Retry-After через cURL в PHP
Для корректной работы с API отправка должна происходить через обертку, анализирующую HTTP-код ответа и структуру JSON. При вызове cURL обязательно проверяем успешность выполнения запроса, валидность JSON и наличие флага ok: false.
Ниже представлен рабочий транспортный компонент для Yii2, реализующий безопасный вызов API с обработкой лимитов:
<?php
namespace app\components;
use Yii;
use yii\base\Component;
use yii\base\Exception;
class TelegramTransport extends Component
{
private string $token;
public function init(): void
{
parent::init();
$this->token = (string) (getenv('TELEGRAM_BOT_TOKEN') ?: Yii::$app->params['telegramBotToken'] ?? '');
if (empty($this->token)) {
throw new Exception('TELEGRAM_BOT_TOKEN is not configured in environment or params.');
}
}
public function execute(string $method, array $params = []): array
{
$url = "https://api.telegram.org/bot{$this->token}/{$method}";
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($params),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_CONNECTTIMEOUT => 5,
]);
$response = curl_exec($ch);
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($response === false) {
throw new Exception("Telegram cURL Error: {$curlError}");
}
$data = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception("Invalid JSON response from Telegram API: {$response}");
}
if ($httpCode === 429) {
$retryAfter = (int) ($data['parameters']['retry_after'] ?? 3);
Yii::warning("Telegram Rate Limit 429 hit. Retry after {$retryAfter} sec.", 'telegram');
return [
'ok' => false,
'error_code' => 429,
'retry_after' => $retryAfter,
'description' => $data['description'] ?? 'Too Many Requests',
];
}
if ($httpCode !== 200 || !($data['ok'] ?? false)) {
Yii::error("Telegram API Error [{$httpCode}]: " . json_encode($data), 'telegram');
}
return $data;
}
}Архитектура очереди исходящих сообщений в Yii2
Чтобы соблюдать ограничение «1 сообщение в секунду в один чат» и «30 сообщений в секунду всего», отправку необходимо вынести в фоновые задачи (Yii Queue + Redis). При получении ошибки 429 задача не должна помечаться как сбойная — её следует отложить на указанный интервал retry_after.
Создадим класс задачи очереди (Job), который использует Redis-блокировку для предотвращения слишком частой отправки в один и тот же чат:
<?php
namespace app\jobs;
use Yii;
use yii\base\BaseObject;
use yii\queue\JobInterface;
use app\components\TelegramTransport;
class SendTelegramMessageJob extends BaseObject implements JobInterface
{
public int $chatId;
public string $text;
public ?array $replyMarkup = null;
public function execute($queue): void
{
$redis = Yii::$app->redis;
$lockKey = "tg_rate_limit:chat:{$this->chatId}";
// Проверяем, не отправляли ли мы сообщение в этот чат менее 1 секунды назад
if ($redis->get($lockKey)) {
// Откладываем выполнение на 1 секунду
$queue->delay(1)->push($this);
return;
}
/** @var TelegramTransport $transport */
$transport = Yii::$app->get('telegramTransport');
$payload = [
'chat_id' => $this->chatId,
'text' => $this->text,
'parse_mode' => 'HTML',
];
if ($this->replyMarkup !== null) {
$payload['reply_markup'] = $this->replyMarkup;
}
$result = $transport->execute('sendMessage', $payload);
if (isset($result['error_code']) && $result['error_code'] === 429) {
$delay = (int) ($result['retry_after'] ?? 3);
// Возвращаем задачу в очередь с задержкой Retry-After + 1 секунда
$queue->delay($delay + 1)->push($this);
return;
}
if (($result['ok'] ?? false) === true) {
// Ставим блокировку на 1 секунду для защиты от частых вызовов
$redis->setex($lockKey, 1, '1');
}
}
}Группировка сообщения в один чат (Message Aggregation)
Если ваша CRM или система аналитики генерирует десятки событий в минуту (например, сменился статус заказа, поступила оплата, назначен курьер), отправка отдельного сообщения на каждый чих гарантированно приведет к 429 ошибке. Решение — накапливать уведомления в буфер (Redis) и отправлять их единым блоком.
<?php
namespace app\components;
use Yii;
use app\jobs\SendTelegramMessageJob;
class TelegramBufferManager
{
public static function pushNotification(int $chatId, string $messageLine): void
{
$redis = Yii::$app->redis;
$key = "tg_buffer:{$chatId}";
$redis->rpush($key, $messageLine);
// Задаем время жизни буфера 60 секунд
$redis->expire($key, 60);
}
public static function flush(int $chatId): void
{
$redis = Yii::$app->redis;
$key = "tg_buffer:{$chatId}";
$items = [];
while ($item = $redis->lpop($key)) {
$items[] = $item;
}
if (empty($items)) {
return;
}
// Объединяем сообщения через разделитель
$combinedText = implode("
---
", $items);
// Отправляем склеенный текст в очередь
Yii::$app->queue->push(new SendTelegramMessageJob([
'chatId' => $chatId,
'text' => $combinedText,
]));
}
}Что ломается при массовых рассылках и как этого избежать
Во время проведения промо-рассылок по базе пользователей вы обязательно столкнетесь со следующими проблемами:
- Зависание Webhook-обработчика: Если во время рассылки пользователь пишет боту, а бэкенд пытается сразу ответить ему в синхронном режиме, обработка вебхука отключается по тайм-ауту (стандартно 5–10 секунд в Telegram). Telegram начинает повторять Webhook-запросы, создавая эффекты «шторма». Решение: всегда возвращайте
HTTP 200 OKза 50 миллисекунд, передавая полученныйupdate_idв фоновый воркер. - Удаление бота или блокировка пользователем: При рассылке API возвращает
403 Forbidden: bot was blocked by the user. Не пытайся повторять отправку таким пользователям. Помечайте их в БД флагомis_active = false, чтобы не тратить лимиты API впустую. - Превышение длины текста: При склейке сообщений или динамической генерации текстов вы можете превысить 4096 символов. Текст необходимо нарезать на чанки перед помещением в очередь.
Подводя итог: Никогда не слать входящие и исходящие вызовы API напрямую из контроллеров. Очередь задач с дроттлингом, проверка retry_after и ведение статусов активности пользователей — единственный способ обеспечить стабильную работу проекта при миллионных охватах.Если вам необходима профессиональная разработка высоконагруженных ботов или Mini Apps, обратитесь к специалистам на botservice.biz.