Що ми збираємо
Telegram Bot API обмежує частоту запитів через чат і в усьому світі. Якщо ви перевищите його, ви отримаєте HTTP 429 Занадто багато запитів з retry_after в секундах — або тихі втрати під час розсилок. На цьому уроці:
- правильний синтаксичний аналіз
429іretry_afterу PHP через cURL; - невелика вихідна черга з серіалізацією надсилання чату;
- буферні сплески в одному чаті з урахуванням ~1 повідомлення в секунду;
- типові поломки в масових розсилках.
Це не огляд фреймворків, а робочий шаблон PHP поруч із обробником вебхуків.
Обмеження, яких слід дотримуватися
Офіційна документація є джерелом істини. На практиці:
- ~1 повідомлення в секунду в одному приватному чаті; ~20 повідомлень в хвилину в одній групі;
- ~30 повідомлень в секунду по всьому світу на бота (короткі спалахи є прийнятними);
sendMessageта сусідні методи повертають429з полем JSONparameters.retry_afterСекунди
Ігноруйте ліміти — Telegram почне дроселювати, черга зростатиме, розсилка «зламається». Розглянемо retry_after авторитетний: не компенсуйте свій зворотній бік.
1. Обгортка cURL з повторною спробою після
Синтаксичний аналізатор JSON, перевірте ГараздНа 429 - Читайте. retry_after. Не засинайте наосліп протягом певного часу.
<?php
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 для чату та робочого циклу. Кожен чат має свою послідовність. Коли retry_after спати стільки, скільки сказав Telegram; в іншому випадку ~ 1,1 с між відправленнями в одному чаті.
<?php
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->byChat[$chatId] = $jobs;
$this->inflight[$chatId] = true;
try {
tg($method, $params);
} catch (RuntimeException $e) {
if (isset($e->retryAfter)) {
array_unshift($this->byChat[$chatId], [$method, $params]);
sleep(max(1, $e->retryAfter));
} else {
error_log("drop job {$method} chat {$chatId}: " . $e->getMessage());
}
}
$lastPerChat[$chatId] = microtime(true);
unset($this->inflight[$chatId]);
if (empty($this->byChat[$chatId])) {
unset($this->byChat[$chatId]);
}
$progress = true;
}
if (!$progress) {
$idle++;
if ($idle >= $maxIdleRounds) {
break;
}
usleep(200000);
} else {
$idle = 0;
}
// Глобальный потолок ~30/с. При 1.1 с на чат срабатывает редко;
// нагруженным ботам нужен token bucket.
}
}
}
Це навмисно не Redis. Для одного процесу достатньо сотень повідомлень на хвилину. Для декількох працівників — списки Redis за chat_id і атомний поп.
3. Групування повідомлень в одному чаті
Якщо один чат генерує багато невеликих оновлень, не телефонуйте sendMessage для кожної події. Краще:
- буферизувати текст і надсилати його не частіше одного разу на секунду (допомагає пауза в 1,1 секунди);
- для живого статусу —
editMessageTextодне повідомлення замість N нових.
<?php
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 = [];
}
}
Скиньте буфер на таймері ~1 с. Для HTML виведіть на екран вхідні дані: htmlspecialchars($s, ENT_QUOTES | ENT_Replace, 'UTF-8').
4. Що порушує інформаційні бюлетені
- Fanout працює швидше ~30 мсг/с. Наївний цикл абонентів спирається на глобальний ліміт. Проїжджайте через чергу та відро токенів близько 25/с.
- Один і той самий текст у багатьох чатах одночасно. Додати джиттер.
random_int(0,500)мс) перед чергою. - Створіть запис після завантаження. Згенерувати ідентифікатор у
bin2hex(random_bytes(7)), зробіть вставку, а потім поставте сповіщення в чергу. - Курсор
update_idв пам 'яті. Збережіть його. Після перезапуску повторна обробка подвоює вихідний трафік. callback_dataбільше 64 байт. Введіть короткий ідентифікатор, а не весь рядок сутності.- Забули
answerCallbackQuery. Кнопка обертається і виглядає як ліміт швидкості, хоча йдеться не про ліміти.
5. Виробничі примітки
- Нижнє поле
retry_after: навіть з0спати принаймні секунду. - Для декількох процесів підбирайте завдання за допомогою
ВИБРАТИ ... ДЛЯ ОНОВЛЕННЯ ПРОПУСТИТИ ЗАБЛОКОВАНО(PostgreSQL) або стовпецьclaim_at(MySQL). - Автоматичний вимикач: через ~20 послідовно
429— Робітник призупиняється на 30 секунд. - Увійдіть у кожну
429зretry_after,chat_idі метод. - Вебхук повинен відповісти на Telegram за кілька секунд. Розмістіть вихідні дзвінки в черзі та негайно поверніть їх
200— не дзвонитиsendMessageсинхронно в веб-гачку.
Підсумок
Спостереження Повторити спробу - після, тримайте темп для чату та приймайте вихідні дзвінки в чергу. Цього достатньо, щоб бот вижив у списках розсилки та робочому навантаженні.
Якщо боти щодня стикаються з обмеженнями і їм потрібні черги, повторні виклики та нестандартна стимуляція, почніть з botservice.biz.