ЧТО МЫ МОЖЕМ СОЗДАТЬ
API бота Telegram применяет ограничения для каждого чата и каждого метода. Ударьте по ним, и вы получите HTTP Слишком много запросов методом Повторное соединение после: в секундах, или, что еще хуже, тихие капли во время трансляций. В этом уроке мы рассмотрим:
Просмотр 429 и Повторное соединение после: корректно в PHP cURL. - Небольшая исходящая очередь, которая сериализует отправки на чат и глобальные дроссели. - Группировка происходит в одном чате, чтобы избежать потолка в 1 мсг/сек/чат. - Частые поломки при массовых рассылках (один и тот же текст, разветвление, дублирующие рассылки).
Это не рамочный обзор. Это рабочий шаблон PHP, который вы можете перетащить в обработчик веб-перехватчиков.
Ограничения, которые вы должны соблюдать
Telegram публикует их в официальных документах, и они являются источником истины:
- ~1 сообщение в секунду за чат для одного и того же чата и ~20 сообщений в минуту для одной и той же группы. - ~30 сообщений в секунду по всему миру на бота, с разрешенными короткими очередями. - SendMessage и друзья возвращаются 429 с телом JSON, содержащим parameters.retry_after (сек.) getUpdates Длинный опрос Повторное соединение после: различное поведение при обработке.
Если вы проигнорируете их, Telegram задушит вас, ваша очередь вырастет, и ваша трансляция будет выглядеть сломанной. Побалуйте Повторное соединение после: как авторитетный; не изобретайте свой собственный задний ход.
1. Обертка cURL, которая покрывает Повторное соединение после:
API бота возвращает JSON. Разберите его, проверьте OKПо 429 Захват Повторное соединение после:. Не просто спать фиксированное значение.
function tg(string $method, array $params): array {
$token = getenv('TG_BOT_TOKEN');
$ch = curl_init("https://api.telegram.org/bot{$token}/{$method}");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 15,
CURLOPT_POSTFIELDS => http_build_query($params),
CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($body === false) {
$err = curl_error($ch);
curl_close($ch);
throw new RuntimeException("cURL error: {$err}");
}
curl_close($ch);
$decoded = json_decode($body, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException("Bad JSON: " . json_last_error_msg());
}
if ($status === 429 && is_array($decoded)) {
$retry = $decoded['parameters']['retry_after'] ?? 1;
$err = new RuntimeException("rate limited: retry after {$retry}s");
$err->retryAfter = (int) $retry;
throw $err;
}
if (empty($decoded['ok'])) {
throw new RuntimeException("TG error: " . ($decoded['description'] ?? 'unknown'));
}
return $decoded['result'];
}
Обратите внимание на преднамеренное использование getenv('TG_BOT_TOKEN') — никогда жестко не кодировать токен. http_build_query избегает сюрпризов с parse_mode=HTML параметров и сил в процентах.
2. Исходящая очередь с ключом chat_id
Самая простая правильная очередь - это FIFO для каждого чата с небольшим рабочим циклом. Каждый чат получает свою последовательность; рабочий спит Повторное соединение после: когда Telegram так говорит, в противном случае ~ 1.1s между отправками в один и тот же чат.
final class TgQueue {
/** @var array<int, array<int, array{0:string,1:array}>> */
private array $byChat = [];
/** @var array<int, true> */
private array $inflight = [];
public function enqueue(int $chatId, string $method, array $params): void {
$this->byChat[$chatId][] = [$method, $params];
}
public function run(int $maxIdleRounds = 5): void {
$idle = 0;
$lastPerChat = [];
while (!empty($this->byChat) || !empty($this->inflight)) {
$progress = false;
foreach ($this->byChat as $chatId => $jobs) {
if (isset($this->inflight[$chatId])) continue;
$now = microtime(true);
if (isset($lastPerChat[$chatId]) && $now - $lastPerChat[$chatId] < 1.1) continue;
[$method, $params] = array_shift($jobs);
$this->inflight[$chatId] = true;
try {
tg($method, $params);
} catch (RuntimeException $e) {
if (isset($e->retryAfter)) {
array_unshift($this->byChat[$chatId], [$method, $params]);
sleep($e->retryAfter);
} else {
// non-recoverable: drop or persist for review
error_log("drop job {$method} chat {$chatId}: " . $e->getMessage());
}
}
$lastPerChat[$chatId] = microtime(true);
unset($this->inflight[$chatId]);
if (empty($jobs)) unset($this->byChat[$chatId]);
$progress = true;
}
if (!$progress) { $idle++; if ($idle >= $maxIdleRounds) break; usleep(200000); }
else $idle = 0;
// global cap: ~30/s. With 1.1s per chat and many chats this rarely fires,
// but a busy bot should add a token bucket here.
}
}
}
Это намеренно не очередь Redis. Для однопроцессного веб-перехватчика, который обрабатывает несколько сотен сообщений в минуту, этого достаточно. Для производства с несколькими рабочими замените массивы в памяти списками Redis с ключом ID чата и используйте скрипт Lua для Atomic Pop.
3. Группировка сообщений в одном чате
Если у вас есть чат, в котором вы производите много небольших обновлений (например, лента статуса), не звоните SendMessage один раз на мероприятие. Используйте один из следующих вариантов:
- Буферный текст в приложении и звонок SendMessage не чаще одного раза в секунду для этого чата. Вышеприведенный охранник 1.1s уже применяет это. - Используйте sendMessageGroup только если он вам действительно нужен; стандартный API бота не раскрывает его. Законный трюк «групповых сообщений в одном чате» заключается в том, чтобы буферизовать клиентскую сторону и отправить одно более богатое сообщение. - Для длительных обновлений используйте editMessageText на одном закрепленном сообщении вместо отправки N новых сообщений.
Помощник минимальной буферизации:
final class ChatBuffer {
private array $pending = []; // chatId => string
public function push(int $chatId, string $text): void {
$this->pending[$chatId] = ($this->pending[$chatId] ?? '') . $text;
}
public function flush(TgQueue $q): void {
foreach ($this->pending as $chatId => $text) {
$q->enqueue($chatId, 'sendMessage', [
'chat_id' => $chatId,
'text' => $text,
'parse_mode' => 'HTML',
]);
}
$this->pending = [];
}
}
Скомбинировать ChatBuffer::flush() с 1-секундным таймером. Для полезных нагрузок HTML избегайте ввода пользователем с помощью htmlspecialchars($s, ENT_QUOTED | ENT_REPLUTE, 'UTF-8') перед конкатенацией; API бота отклонит неэкранированные < и > hommes.
4. Что прерывается во время трансляций
Трансляции - это то место, где команды сильнее всего бьют по лимитам. Типичные виды отказов:
- Скорость разветвления более 30 мсг/с. Наивная петля, которая вызывает SendMessage для каждого последовательного подписчика превысит глобальный лимит, как только вы превысите несколько тысяч получателей. Выполните разветвление через очередь и добавьте ведро с маркером с ограничением около 25/с, чтобы оставить свободное место. - Один и тот же текст отправлен в несколько чатов одновременно. Telegram рассматривает идентичные всплески как подозрительные и может временно заблокировать. Добавьте небольшой дрожание на получателя (random_int(0, 500) мс) перед каждой очередью. - Идентификаторы отведений, сгенерированные после SendMessage. Если ваш поток: создать лид, затем уведомить, и вы пропустите вставку лида в ошибке Telegram, вы потеряете события. Сгенерируйте идентификатор с помощью bin2hex(random_bytes(7)), INSERT сначала, затем поставьте уведомление в очередь. - Однокурсорная дедупликация на веб-перехватчиках. Когда вы также используете getUpdates чтобы восстановиться после сбоев, не полагайтесь на одну память update_id; сохраните его. В противном случае при перезапуске теряется курсор, и вы повторно обрабатываете сообщения, удваивая скорость отправки, когда вы можете себе это позволить. - callback_data более 64 байт. Telegram бесшумно обрезает. Если вы закодируете идентификатор лида в кнопке, сохраните короткий идентификатор, а не полную строку. - Забывчивость answerCallbackQuery. Кнопка «вращается» до тех пор, пока не истечет время ожидания, и вы можете выглядеть ограниченным по скорости, даже если это не так. Всегда отвечайте, даже с пустой строкой.
5. Производственные заметки
Обернуть Повторное соединение после: с небольшим этажом: даже если Telegram вернется 0, поспите по крайней мере одну секунду перед повторной попыткой. - Для многопроцессных рабочих используйте строку базы данных с ВЫБРАТЬ ... ДЛЯ ОБНОВЛЕНИЯ ПРОПУСТИТЬ ЗАБЛОКИРОВАНО (PostgreSQL) или claimed_at столбца (MySQL), чтобы сделать очередь за чат транзакционной. - Добавьте автоматический выключатель: если вы наблюдаете 20 последовательных 429с, приостановите работника на 30 секунд. Telegram время от времени ужесточает лимиты на шумных ботов. - Log every 429 где Повторное соединение после:, ID чата, и галогенов. Узоры легче обнаружить в логах, чем в дашбордах. - Помните, что вебхуки должны отвечать на Telegram в течение нескольких секунд. Не звонить SendMessage синхронно внутри обработчика веб-перехватчика — постановка в очередь и возврат 200 незамедлительно.
Этих шаблонов достаточно, чтобы отправить бота, который выживает в трансляциях и не падает под нагрузкой. Держите Повторное соединение после: источник правды, а очередь воспринимайте как границу между вашим кодом и темпом Telegram.
---
Если вы поддерживаете ботов, которые ежедневно достигают этих лимитов, и вам нужна студия, которая поставляет такую сантехнику по умолчанию, а не для модернизации, BotCreator создает ботов Telegram и мини-приложения с очередями, повторными попытками и возможностью наблюдения. Дополнительная информация: