Просунуте
Production: черга і retries
При переходе бота в стадию активной эксплуатации (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 выглядит так:
- Принять входящий JSON-запрос от Telegram.
- Быстро валидировать его (токен, IP).
- Записать обновление в очередь (база данных, Redis, RabbitMQ) или просто зафиксировать задачу на отправку.
- Вернуть HTTP-ответ
200 OK. - Фоновый процесс (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