Практична робота з лімітами 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

Розбираємо, що автоматизувати в бізнесі та як це працює на практиці. Без спаму.