Короткий ответ
429 означает ограничение частоты запросов. Сохраните источник ошибки и указанное время ожидания, отложите отклонённую операцию и уменьшите общий темп отправки. Немедленный повтор обычно снова расходует ресурсы и не устраняет причину.
Для ответа Telegram читайте parameters.retry_after. Управляйте скоростью всех отправителей одного бота: пауза только в одном worker-е не поможет, если остальные продолжают ту же рассылку.
1. Найдите ответ Telegram и время ожидания
Сохраните HTTP-статус, заголовок Retry-After и поля JSON ok, error_code, parameters.retry_after. Полный запрос с ключами и текстом сообщения для этой диагностики не нужен.
При прямом обращении к Telegram ограничение обычно приходит с HTTP 429 и JSON с error_code 429. Вымышленный пример тела такого ответа:
{
"ok": false,
"error_code": 429,
"description": "Too Many Requests: retry after 12",
"parameters": {
"retry_after": 12
}
}
parameters.retry_after задаёт время ожидания в секундах до повтора. Используйте структурированное поле, а не извлечение числа из английского текста description. Определение дано в Telegram ResponseParameters.
Если запрос выполняет библиотека, сохраните тело ответа из HTTP-исключения: некоторые клиенты прерывают обычное выполнение при статусе 429. Время ожидания нужно брать из конкретного отказа, а не подставлять фиксированное число для всех сообщений.
HTTP 429 может вернуть и промежуточный сервер. HTML-страница от CDN не является ответом Telegram: сначала определите источник. Таймаут и 502 также требуют отдельной обработки — они не подтверждают отказ по лимиту, и результат отправки может остаться неизвестным.
2. Посчитайте нагрузку на каждом уровне
В FAQ Telegram для обычной отправки приведены ориентиры: избегать более одного сообщения в секунду в один чат, не превышать 20 сообщений в минуту в группу и около 30 сообщений в секунду для массовых уведомлений без платного увеличения лимита.
Это не обещание одинаковой пропускной способности для всех методов и ситуаций. Даже при небольшом среднем потоке возможны короткие всплески или концентрация сообщений в одном чате. При конкретном отказе ориентируйтесь на ответ API и его время ожидания.
- Суммируйте запросы всех worker-ов, cron-задач и экземпляров приложения с одним ботом.
- Учитывайте проверки состояния, операции с файлами и повторные попытки.
- Проверьте, не запускается ли одна рассылка дважды и не включён ли повтор одновременно в SDK, очереди и внешнем обработчике.
- Посмотрите распределение по чатам: отправка в один активный групповой чат может упереться в его ограничение раньше общего.
Например, четыре worker-а по одному сообщению в секунду могут отправлять в один чат четыре сообщения в секунду. Каждый процесс по отдельности выглядит ненагруженным, но Telegram видит их суммарную активность. Ограничитель должен быть общим для этих процессов.
3. Отложите запрос и уменьшите скорость
Для уведомлений удобна очередь с плановым временем следующей попытки. Алгоритм:
- Подтвердите отказ именно по лимиту и определите его источник.
- Получите parameters.retry_after из ответа Telegram. Если применяется ещё один независимый лимит, соблюдайте и его время ожидания.
- Сохраните следующую попытку не раньше этого времени. Можно добавить небольшой случайный запас, но нельзя уменьшать требуемую паузу.
- Освободите worker. Не держите десятки процессов в sleep, ожидающих окончания одной паузы.
- Ограничьте число попыток и срок актуальности уведомления. После исчерпания бюджета оставьте запись для разбора.
Отложить только отклонённое задание недостаточно, если другие процессы продолжают отправку в тот же лимит. Храните общий cooldown в Redis или БД: для конкретного бота и, где это требуется, получателя. Проверка и резервирование очередного разрешённого слота должны учитывать конкурирующие процессы.
Если область Telegram-ограничения неизвестна, временная пауза для отправок этого бота — более осторожный вариант, чем попытка продолжить все соседние задания. После паузы возвращайтесь к равномерному темпу, а не выпускайте накопившуюся очередь одним залпом.
Если время ожидания отсутствует или некорректно, не подставляйте автоматически ноль. Приостановите операцию для разбора либо примените заранее установленную ограниченную политику задержек. Уведомление о давно истёкшем событии может быть правильнее пометить как устаревшее, чем повторять бесконечно.
В Laravel для отложенной попытки можно использовать release($delay), в других очередях — их механизм планирования. Само наличие очереди не задаёт темп отправки: дополнительно нужен общий контроль скорости.
Пример разбора ответа на PHP без SDK
Следующая функция принимает HTTP-статус и исходное JSON-тело ответа Telegram. Она ничего не отправляет и не запускает повторы. Если ответ не получен, передайте статус 0 и null. Файл telegram-response-decision.php должен начинаться с <?php; тег в блоке опущен.
declare(strict_types=1);
/**
* @return array{state: string, after_seconds: ?int}
*/
function telegramResponseDecision(int $httpStatus, ?string $body): array
{
$unknown = ['state' => 'unknown', 'after_seconds' => null];
if ($body === null || $httpStatus < 200 || $httpStatus >= 500) {
return $unknown;
}
try {
$response = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException) {
return $unknown;
}
if (!is_array($response)) {
return $unknown;
}
if ($httpStatus < 300 && ($response['ok'] ?? null) === true) {
return ['state' => 'sent', 'after_seconds' => null];
}
$code = $response['error_code'] ?? null;
if (($response['ok'] ?? null) !== false || !is_int($code) || $code < 1 || $code >= 500) {
return $unknown;
}
if ($code !== 429) {
return ['state' => 'failed', 'after_seconds' => null];
}
$parameters = is_array($response['parameters'] ?? null) ? $response['parameters'] : [];
$wait = $parameters['retry_after'] ?? null;
if (!is_int($wait) || $wait < 0 || $wait > PHP_INT_MAX - 3) {
return ['state' => 'paused', 'after_seconds' => null];
}
return [
'state' => 'retry',
'after_seconds' => max(1, $wait) + random_int(1, 3),
];
}
Проверить функцию можно на вымышленном ответе, без запросов в Telegram:
$body = '{"ok":false,"error_code":429,"parameters":{"retry_after":12}}';
$decision = telegramResponseDecision(429, $body);
// state = retry; after_seconds — от 13 до 15 секунд.
Небольшой случайный запас добавляется к указанной паузе. Значение retry_after, равное нулю, не превращается в плотный цикл. Строка вместо числа, отрицательное значение и отсутствие задержки дают состояние paused для разбора.
sent— отправка подтверждена.retry— сохраните время следующей попытки, обновите общую паузу и верните задание планировщику в пределах бюджета попыток.paused— лимит подтверждён, но понятной задержки нет; требуется разбор или заранее выбранная ограниченная политика ожидания.failed— исправьте запрос или настройки.unknown— результат не установлен; автоматический повтор отправки не разрешён этой политикой.
Это разбор ответа обычного sendMessage, а не реализация очереди. Вызов функции не отправляет сообщение повторно и не защищает от двух одновременно работающих отправителей. Планирование, общий ограничитель, срок актуальности и учёт попыток остаются в приложении.
Что не исправляет ошибку 429
- Увеличение сетевого таймаута. Сервер уже ответил отказом по частоте; ждать тот же ответ дольше не нужно.
- Больше worker-ов. Без общего ограничителя это увеличивает суммарный поток и число отказов.
- Немедленный повтор в цикле. Создаёт новые запросы до окончания требуемой паузы.
- Замена прокси или ротация ключей ради обхода. Лимит отправки Telegram не исчезает от смены сетевого маршрута.
- Удаление всей очереди. Это потеря уведомлений, а не управление скоростью. Устаревшие записи следует обрабатывать по правилам продукта.
Если сообщение приходит дважды, отдельно проверьте повторную постановку задания и неопределённые результаты отправки. 429 и дубли могут присутствовать одновременно, но у них не обязательно одна причина.
Как проверить обработку без реальной перегрузки
Подставьте в тестах несколько ответов: успешный JSON, Telegram 429 с retry_after, лимит без задержки, ошибку параметров с кодом 400, отказ 401, HTML вместо JSON, 502 и отсутствие ответа. Проверьте, что только явный лимит с понятной паузой разрешает отложенный повтор.
Затем проверьте общий ограничитель с двумя worker-ами: пауза, установленная одним, должна учитываться другим. Убедитесь, что длительное ожидание не занимает процесс, а превышение числа попыток или срока уведомления завершает обработку.
В рабочем режиме наблюдайте число 429 по источнику, длину очереди, возраст самого старого задания и долю успешных отправок. Исчезновение 429 ценой бесконечно растущей очереди не означает, что доставка восстановлена: нужно сопоставить скорость поступления и доступную скорость отправки.
Если вы используете BotGate
Через BotGate нужно различать два источника ограничений. Telegram-ошибка сохраняется в JSON, но приходит с HTTP 400; лимит самого сервиса возвращается с HTTP 429. Поэтому одной проверки HTTP-статуса недостаточно.
Ограничение BotGate
При превышении лимита самого шлюза ответ другой:
HTTP/1.1 429 Too Many Requests
Retry-After: 18
Content-Type: application/json
{"ok":false,"error":"Превышен лимит запросов"}
Для отдельного бота текст ошибки — «Превышен лимит запросов для этого бота». Здесь время берётся из числового HTTP-заголовка Retry-After; поля parameters.retry_after может не быть. Это разные места хранения одной по смыслу подсказки.
Для Telegram ищите ok: false, error_code: 429 и parameters.retry_after. Для собственного лимита BotGate время передаётся в числовом заголовке Retry-After. При применении нескольких ограничений соблюдайте каждую паузу.
Базовые ограничения сервиса — 600 запросов в минуту на API-ключ и 300 на бота. Учитываются запросы API, включая диагностику и повторы; актуальные условия описаны в документации. Для лимита ключа нужна общая пауза всех использующих его отправителей, даже если боты разные.
Разбор через PHP SDK
SDK предоставляет отдельные исключения для этих случаев:
RateLimitException— HTTP 429 от BotGate. МетодretryAfter()читает числовой заголовок Retry-After.TelegramExceptionсtelegramErrorCode() === 429— лимит Telegram внутри HTTP 400. МетодretryAfter()читает целое неотрицательное значение из parameters.retry_after;parameters()сохраняет параметры ошибки.
В обоих случаях неизвестное или некорректное время возвращается как null. У TelegramException httpStatusCode() и getCode() по-прежнему содержат HTTP 400 — для определения Telegram 429 используйте telegramErrorCode().
Стандартный транспорт автоматически повторяет HTTP 429, 502, 503 и 504, учитывая числовой Retry-After. Telegram 429 внутри HTTP 400 он автоматически не повторяет. Для собственного планировщика отключите встроенные повторы через maxRetries: 0, а в Laravel — BOTGATE_RETRY_MAX=0. Иначе обработчик увидит HTTP 429 BotGate только после завершения встроенных попыток.
Установка SDK: composer require botgate/sdk. Код ниже использует эти исключения, поэтому читать исходный HTTP-ответ вручную не требуется.
Пример обработки через PHP SDK
Следующая функция получает исключение SDK и возвращает решение для очереди. Она сама ничего не отправляет и не повторяет. Файл delivery-decision.php должен начинаться с <?php; тег в примере опущен.
declare(strict_types=1);
use BotGate\Exception\BotGateException;
use BotGate\Exception\RateLimitException;
use BotGate\Exception\TelegramException;
/**
* @return array{state: string, source: ?string, after_seconds: ?int}
*/
function telegramFailureDecision(BotGateException $error): array
{
if ($error instanceof RateLimitException) {
$source = 'botgate';
$wait = $error->retryAfter();
} elseif ($error instanceof TelegramException) {
if ($error->telegramErrorCode() !== 429) {
return ['state' => 'failed', 'source' => 'telegram', 'after_seconds' => null];
}
$source = 'telegram';
$wait = $error->retryAfter();
} else {
$status = $error->httpStatusCode();
$state = $status >= 400 && $status < 500 ? 'failed' : 'unknown';
return ['state' => $state, 'source' => null, 'after_seconds' => null];
}
if ($wait === null || $wait < 0) {
return ['state' => 'paused', 'source' => $source, 'after_seconds' => null];
}
return [
'state' => 'retry',
'source' => $source,
'after_seconds' => max(1, $wait) + random_int(1, 3),
];
}
Небольшой случайный запас добавляется к паузе, а не заменяет её. Для Telegram функция проверяет код 429: ошибка параметров или прав бота не должна разрешать повтор только из-за наличия поля retry_after.
Теперь создайте send-with-decision.php рядом с этой функцией и bootstrap.php из руководства PHP. Bootstrap загружает Composer, проверяет конфигурацию и создаёт $bot с maxRetries: 0. Следующий фрагмент выполняет одну реальную отправку в настроенный тестовый чат:
declare(strict_types=1);
use BotGate\Exception\BotGateException;
require __DIR__.'/bootstrap.php';
require __DIR__.'/delivery-decision.php';
try {
$bot->call('sendMessage', [
'chat_id' => $requiredEnv('TELEGRAM_CHAT_ID'),
'text' => 'Тест обработки ответа',
]);
$decision = ['state' => 'sent', 'source' => null, 'after_seconds' => null];
} catch (BotGateException $error) {
$decision = telegramFailureDecision($error);
}
После запроса сохраните решение в своём журнале уведомлений или передайте его планировщику:
sent— зафиксируйте подтверждённый успех.retry— сохраните время следующей попытки и обновите общий cooldown для источникаsource; повторяйте только в пределах бюджета попыток.paused— есть лимит, но нет понятного времени ожидания; автоматический немедленный повтор запрещён этой политикой.failed— требуется исправить запрос или настройки.unknown— результат не установлен; не отправляйте сообщение заново без выбранной процедуры восстановления.
Функция не заменяет очередь, дедупликацию и учёт срока актуальности. При любом новом отказе нужно заново прочитать время ожидания, а не повторять по старому значению. Транспортные ошибки и 5xx намеренно не превращаются в разрешение на автоматическую отправку. Пример отложенного release() есть в статье для Laravel.
Источники и границы примера
- Telegram Bot FAQ — ориентиры частоты отправки.
- Telegram ResponseParameters — поле retry_after.
- BotGate PHP SDK — повторы транспорта и разбор ошибок.
- Документация BotGate — ошибки и ограничения сервиса.
Числа в примерах ответов вымышлены. Реальное время ожидания берите из конкретного ответа.