Ліміти 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

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