При масштабуванні Telegram-бота або запуску масових сповіщень розробники неминуче стикаються з жорсткими лімітами Telegram Bot API. Ігнорування правил платформи призводить до помилок із кодом HTTP 429, тимчасового блокування відправки повідомлень або повної заморозки роботи бота під час пікових навантажень.
У цій статті ми розберемо реальні ліміти сервера Telegram, побудуємо збоєстійкий HTTP-клієнт на PHP для обробки відповідей Retry-After, спроєктуємо систему черг для вихідних повідомлень і розглянемо патерни групування даних, щоб не перевищувати допустиму частоту викликів.
Офіційні та приховані ліміти Telegram Bot API
Документація Telegram описує основні ліміти, але на практиці їхня поведінка має нюанси, які необхідно враховувати під час проєктування архітектури:
- Загальний ліміт відправки: не більше 30 повідомлень на секунду сумарно по всіх чатах для одного токена бота.
- Ліміт на особистий чат: не більше 1 повідомлення на секунду в один і той самий приватний діалог з користувачем.
- Групи та канали: не більше 20 повідомлень на хвилину в одну й ту саму групу або канал.
- Массовые рассылки: при відправці довгих ланцюжків сповіщень рекомендована швидкість становить не більше 25–30 повідомлень на секунду, навіть якщо повідомлення надсилаються різним користувачам.
- Payload webhook: розмір тіла вхідного запиту від Telegram обмежений, а час очікування відповіді від вашого сервера (timeout) становить 10–60 секунд. Якщо бекенд не відповість кодом 200 OK вчасно, Telegram почне повторно надсилати ті самі
update_id.
Якщо бот перевищує допустиму частоту викликів, API повертає HTTP-статус 429 Too Many Requests із JSON-відповіддю, що містить поле parameters.retry_after. Це значення вказує, скільки секунд потрібно зачекати перед виконанням наступного запиту.
Збоєстійкий cURL-клієнт з обробкою HTTP 429
Ніколи не використовуйте file_get_contents для роботи з Telegram API, оскільки ця функція не дозволяє гнучко обробляти заголовки HTTP, таймаути та коди помилок рівня REST. Нижче наведено сервісний клас на PHP, що реалізує правильну відправку через cURL з автоматичним перехопленням 429 і повторною спробою після паузи.
<?php
class TelegramApiClient
{
private string $token;
private int $maxRetries;
public function __construct(?string $token = null, int $maxRetries = 3)
{
$this->token = $token ?? (string) getenv('TELEGRAM_BOT_TOKEN');
$this->maxRetries = $maxRetries;
if (empty($this->token)) {
throw new InvalidArgumentException('Telegram bot token is missing.');
}
}
public function execute(string $method, array $params = []): array
{
$url = sprintf('https://api.telegram.org/bot%s/%s', $this->token, $method);
$attempt = 0;
while ($attempt < $this->maxRetries) {
$attempt++;
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($params),
CURLOPT_RETURNTRANSFER => true,
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) {
throw new RuntimeException('cURL error: ' . $curlError);
}
$decoded = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException('Invalid JSON response from Telegram API');
}
if ($httpCode === 429) {
$retryAfter = $decoded['parameters']['retry_after'] ?? 1;
sleep((int) $retryAfter);
continue;
}
if ($httpCode === 403) {
// Пользователь заблокировал бота или бот был удален из чата
return [
'ok' => false,
'error_code' => 403,
'description' => $decoded['description'] ?? 'Forbidden',
];
}
if (isset($decoded['ok']) && $decoded['ok'] === true) {
return $decoded;
}
return $decoded;
}
throw new RuntimeException('Exceeded maximum retry attempts for method: ' . $method);
}
}
Архітектура черги вихідних повідомлень (Rate Limiter)
Викликати метод API напряму з обробника Webhook не можна: при масовому припливу користувачів або виконанні розсилки вебсервер вичерпает ліміт з'єднань, а Telegram тимчасово заблокує відправку. Правильний підхід — використовувати асинхронну чергу завдань (на базі Redis, RabbitMQ або таблиці в БД) із фоновим воркером.
Воркер повинен контролювати дві паузи: глобальний інтервал (не більше 30 запитів на секунду) і персональний інтервал на чат (не менше 1.05 секунди між повідомленнями одному користувачеві).
<?php
class MessageQueueWorker
{
private TelegramApiClient $apiClient;
private PDO $db;
public function __construct(TelegramApiClient $apiClient, PDO $db)
{
$this->apiClient = $apiClient;
$this->db = $db;
}
public function processNext(): bool
{
// Выбираем из базы наиболее старое непрочитанное сообщение
$stmt = $this->db->prepare("
SELECT * FROM bot_outbox
WHERE status = 'pending' AND scheduled_at <= NOW()
ORDER BY id ASC LIMIT 1 FOR UPDATE SKIP LOCKED
");
$stmt->execute();
$job = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$job) {
return false;
}
$chatId = $job['chat_id'];
// Проверяем время последней отправки в этот конкретный чат
$lastSentStmt = $this->db->prepare("SELECT last_sent_at FROM bot_chats WHERE chat_id = :chat_id");
$lastSentStmt->execute([':chat_id' => $chatId]);
$lastSentAt = $lastSentStmt->fetchColumn();
if ($lastSentAt) {
$timePassed = microtime(true) - (float) $lastSentAt;
if ($timePassed < 1.05) {
// Задерживаем обработку для соблюдения лимита 1 сообщ/сек на чат
usleep((int) ((1.05 - $timePassed) * 1000000));
}
}
$payload = json_decode($job['payload'], true);
$result = $this->apiClient->execute('sendMessage', $payload);
if (isset($result['ok']) && $result['ok'] === true) {
$this->markAsSent($job['id']);
$this->updateLastSentTime($chatId, microtime(true));
} else {
$errorCode = $result['error_code'] ?? 0;
if ($errorCode === 403) {
// Деактивируем пользователя, если он заблокировал бота
$this->markUserAsBlocked($chatId);
$this->markAsFailed($job['id'], 'User blocked bot');
} else {
$this->markAsFailed($job['id'], $result['description'] ?? 'Unknown error');
}
}
// Глобальный паузер между любыми исходящими запросами (максимум 30 TPS)
usleep(35000); // ~35 мс пауза = до 28 запросов в секунду
return true;
}
private function markAsSent(int $id): void
{
$stmt = $this->db->prepare("UPDATE bot_outbox SET status = 'sent', sent_at = NOW() WHERE id = :id");
$stmt->execute([':id' => $id]);
}
private function markAsFailed(int $id, string $reason): void
{
$stmt = $this->db->prepare("UPDATE bot_outbox SET status = 'failed', error_message = :err WHERE id = :id");
$stmt->execute([':err' => $reason, ':id' => $id]);
}
private function updateLastSentTime(int $chatId, float $microtime): void
{
$stmt = $this->db->prepare("
INSERT INTO bot_chats (chat_id, last_sent_at) VALUES (:chat_id, :last_sent)
ON DUPLICATE KEY UPDATE last_sent_at = :last_sent
");
$stmt->execute([':chat_id' => $chatId, ':last_sent' => $microtime]);
}
private function markUserAsBlocked(int $chatId): void
{
$stmt = $this->db->prepare("UPDATE bot_users SET is_active = 0 WHERE chat_id = :chat_id");
$stmt->execute([':chat_id' => $chatId]);
}
}
Групування та батчинг повідомлень в один чат
Поширена помилка — надсилання кількох пов'язаних повідомлень поспіль (наприклад, текстовий заголовок, потім фото, а слідом блок із деталями замовлення). Це витрачає ліміт 1 повідомл/сек і змушує користувача читати уривки інформації.
1. Об'єднання тексту та клавіатури
Формуйте єдиний HTML/Markdown-шаблон з усіма даними замовлення та прикріплюйте до нього inline-клавіатуру за один виклик sendMessage. Максимальна довжина тексту повідомлення в Telegram становить 4096 символів, чого достатньо для більшості бізнес-сценаріїв.
2. Використання sendMediaGroup
Якщо необхідно надіслати кілька фотографій або документів, групуйте їх за допомогою методу sendMediaGroup (від 2 до 10 файлів в одному альбомі). Це вважається за 1 запит до API Telegram.
Що ламається при масових розсилках і як із цим боротися
Під час відправки сповіщень по великій базі (від 10 000 адресатів і вище) ви обов'язково зіткнетеся з такими ситуаціями:
- Хвиля помилок 403 Forbidden: частина користувачів видалить бота або заблокує його. Якщо воркер продовжить закидати API запитами до цих користувачів, платформа знизить загальний рейтинг довіри до бота. При отриманні 403 вимикайте відправку для даного
chat_idу БД миттєво. - Каскадний 429: якщо воркер отримує 429 і у вашій системі запущено кілька паралельних процесів відправки, вони всі почнуть отримувати 429 одночасно. Використовуйте спільну паузу в Redis (Rate Limiter Circuit Breaker), щоб у разі виникнення 429 в одному воркері всі інші процеси завмирали на вказаний паузою
retry_afterінтервал. - Забивання вебхука під час розсилки: відправка не повинна блокувати синхронну обробку вхідних команд користувачів. Розділяйте черги: створюйте високий пріоритет для відповідей на вхідні повідомлення (наприклад, натискання кнопки) і низький пріоритет для фонових розсилок.
Розробка збоєстійких Telegram-бот ів та інтеграцій будь-якої складності — BotCreator.