Практическая работа с лимитами Telegram Bot API: 429 Retry-After, очереди на Yii2 и массовые рассылки

Разработчики 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,
]));
}
}

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

Во время проведения промо-рассылок по базе пользователей вы обязательно столкнетесь со следующими проблемами:

  1. Зависание Webhook-обработчика: Если во время рассылки пользователь пишет боту, а бэкенд пытается сразу ответить ему в синхронном режиме, обработка вебхука отключается по тайм-ауту (стандартно 5–10 секунд в Telegram). Telegram начинает повторять Webhook-запросы, создавая эффекты «шторма». Решение: всегда возвращайте HTTP 200 OK за 50 миллисекунд, передавая полученный update_id в фоновый воркер.
  2. Удаление бота или блокировка пользователем: При рассылке API возвращает 403 Forbidden: bot was blocked by the user. Не пытайся повторять отправку таким пользователям. Помечайте их в БД флагом is_active = false, чтобы не тратить лимиты API впустую.
  3. Превышение длины текста: При склейке сообщений или динамической генерации текстов вы можете превысить 4096 символов. Текст необходимо нарезать на чанки перед помещением в очередь.
Подводя итог: Никогда не слать входящие и исходящие вызовы API напрямую из контроллеров. Очередь задач с дроттлингом, проверка retry_after и ведение статусов активности пользователей — единственный способ обеспечить стабильную работу проекта при миллионных охватах.

Если вам необходима профессиональная разработка высоконагруженных ботов или Mini Apps, обратитесь к специалистам на botservice.biz.

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

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