Продвинутое

Production: очередь и retries

Организация очереди сообщений и обработка ошибок 429 (Too Many Requests) в Telegram Bot API на PHP. Создание надежного воркера для продакшена.

При переходе бота в стадию активной эксплуатации (Production) вы неизбежно столкнетесь с ограничениями Telegram Bot API на отправку сообщений (Rate Limits), а также с необходимостью обрабатывать временные сбои сети и серверов Telegram.

Если ваш бот отправляет рассылки или обслуживает тысячи пользователей одновременно, отправка сообщений напрямую внутри вебхука приведет к зависанию PHP-процессов, превышению лимитов и потере обновлений. Решением этой проблемы является архитектура с использованием очередей сообщений и механизма повторных попыток (retries).

Лимиты Telegram Bot API (Rate Limits)

Telegram жестко ограничивает частоту отправки запросов к API. Основные лимиты, которые необходимо учитывать:

  • В один чат (приватный или групповой): не более 1 сообщения в секунду.
  • Группы и супергруппы: не более 20 сообщений в минуту.
  • Общий лимит для бота: не более 30 сообщений в секунду для всех запросов.

При превышении этих лимитов Telegram возвращает HTTP-код 429 Too Many Requests и JSON-ответ с описанием ошибки, содержащий поле parameters.retry_after. Это число секунд, которое необходимо подождать перед повторным запросом.

{
"ok": false,
"description": "Too Many Requests: retry after 9",
"error_code": 429,
"parameters": {
"retry_after": 9
}
}

Почему вебхук не должен отправлять сообщения

Когда Telegram отправляет обновление на ваш Webhook, ваш скрипт должен отработать максимально быстро (желательно до 100 миллисекунд) и вернуть HTTP-ответ 200 OK. Если ваш скрипт начинает внутри вебхука делать тяжелые cURL-запросы, слать медиафайлы или ожидать таймауты при ошибках 429, Telegram посчитает, что ваш сервер недоступен, разорвет соединение и начнет присылать то же самое обновление повторно.

Правильная архитектура вебхука в Production выглядит так:

  1. Принять входящий JSON-запрос от Telegram.
  2. Быстро валидировать его (токен, IP).
  3. Записать обновление в очередь (база данных, Redis, RabbitMQ) или просто зафиксировать задачу на отправку.
  4. Вернуть HTTP-ответ 200 OK.
  5. Фоновый процесс (Worker) берет задачи из очереди и отправляет их в Telegram с контролем лимитов.

Простейшая очередь сообщений на MySQL

Для небольших и средних проектов не обязательно сразу разворачивать сложные брокеры очередей вроде RabbitMQ. Достаточно таблицы в базе данных, которая будет выполнять роль буфера сообщений.

Создадим таблицу bot_messages_queue для хранения исходящих сообщений:

CREATE TABLE `bot_messages_queue` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`chat_id` BIGINT NOT NULL,
`text` TEXT NOT NULL,
`status` VARCHAR(20) DEFAULT 'pending', -- pending, sent, failed
`retry_count` INT DEFAULT 0,
`send_after` DATETIME NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Реализация фонового обработчика (Worker)

Фоновый скрипт запускается через cron (например, каждую минуту) или работает как демон (systemd-сервис) и обрабатывает записи из таблицы очередей.

Для отправки запросов мы будем использовать надежный HTTP-клиент. Подробнее о структуре отправки запросов через cURL вы можете прочитать в главе HTTP-клиент на PHP.

Ниже представлен пример скрипта-воркера на PHP, который выбирает сообщения из базы данных, отправляет их и корректно обрабатывает ошибку 429 Too Many Requests:

<?php

$db = new PDO('mysql:host=localhost;dbname=bot_db;charset=utf8mb4', 'db_user', 'db_pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]);

$token = getenv('TELEGRAM_BOT_TOKEN');
if (!$token) {
die("TELEGRAM_BOT_TOKEN is not set\n");
}

// Выбираем сообщения, готовые к отправке
$stmt = $db->prepare("SELECT * FROM bot_messages_queue
WHERE status = 'pending'
AND (send_after IS NULL OR send_after <= NOW())
AND retry_count < 5
ORDER BY id ASC
LIMIT 10");
$stmt->execute();
$messages = $stmt->fetchAll();

foreach ($messages as $msg) {
$payload = [
'chat_id' => $msg['chat_id'],
'text' => $msg['text'],
'parse_mode' => 'HTML'
];

$ch = curl_init("https://api.telegram.org/bot{$token}/sendMessage");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($response === false) {
// Ошибка сети — откладываем отправку на 30 секунд
delayMessage($db, $msg['id'], $msg['retry_count'] + 1, 30);
continue;
}

$result = json_decode($response, true);

if ($httpCode === 200 && isset($result['ok']) && $result['ok'] === true) {
// Успешно отправлено
$updateStmt = $db->prepare("UPDATE bot_messages_queue SET status = 'sent' WHERE id = ?");
$updateStmt->execute([$msg['id']]);

// Соблюдаем лимит в 1 сообщение в секунду на один чат
usleep(1000000);
} elseif ($httpCode === 429) {
// Превышен лимит запросов. Получаем время ожидания
$retryAfter = $result['parameters']['retry_after'] ?? 5;

// Откладываем отправку сообщения на указанное время
delayMessage($db, $msg['id'], $msg['retry_count'] + 1, $retryAfter);

// Приостанавливаем работу воркера на время блокировки
sleep($retryAfter);
} else {
// Другая ошибка (например, чат заблокирован пользователем)
$updateStmt = $db->prepare("UPDATE bot_messages_queue SET status = 'failed' WHERE id = ?");
$updateStmt->execute([$msg['id']]);
}
}

function delayMessage(PDO $db, int $id, int $newRetryCount, int $secondsDelay): void {
$sendAfter = date('Y-m-d H:i:s', time() + $secondsDelay);
$stmt = $db->prepare("UPDATE bot_messages_queue
SET retry_count = ?, send_after = ?, status = 'pending'
WHERE id = ?");
$stmt->execute([$newRetryCount, $sendAfter, $id]);
}

Советы для Production-окружения

  • Логирование ошибок: Всегда логируйте ответы API, которые вернули ok: false. Это поможет вовремя обнаружить, что пользователи блокируют бота или что вы используете неверную разметку HTML/Markdown.
  • Идемпотентность: При повторных попытках (retries) из-за сетевых сбоев есть риск отправить одно и то же сообщение дважды. Если критически важно избежать дублей, используйте уникальные идентификаторы транзакций на стороне вашей БД.
  • Мониторинг очереди: Настройте алерты на размер таблицы очередей. Если количество записей со статусом pending лавинообразно растет, значит, ваш бот уперся в лимиты или воркер упал.

Дальше: Заявка с сайта → Telegram