Обновления

Long polling

Long polling в Telegram Bot API: как правильно вызывать getUpdates с timeout, сохранять offset, обрабатывать ошибки и запускать воркер под supervisord.

Long polling — режим, когда ваш PHP-скрипт держит открытое HTTP-соединение с Telegram и ждёт новые события до 30 секунд. В отличие от обычного getUpdates (возвращает ответ сразу, даже если пустой), здесь сервер удерживает запрос, пока не появятся обновления или не истечёт таймаут. Это экономит ресурсы: вместо сотен пустых запросов в секунду — один долгий.

Параметры getUpdates для long polling

Ключевой параметр — timeout (в секундах). Ставьте 20–30. При timeout=0 (по умолчанию) получаете short polling — тысячи пустых ответов. Параметр allowed_updates фильтрует типы событий на стороне Telegram, уменьшая трафик.

Используем функцию telegramApi(), которую мы сделали в главе Первый запрос: getMe. Там мы написали обёртку над cURL с проверкой HTTP-кода, json_last_error и поля ok — здесь просто вызываем её.

<?php
// long_polling.php — однократный запрос для понимания механики
require_once __DIR__ . '/src/TelegramClient.php'; // функция telegramApi()

$token = getenv('TELEGRAM_BOT_TOKEN');
if (!$token) {
die('TELEGRAM_BOT_TOKEN не задан в окружении\n');
}

$offset = 0; // первый запуск — берём всё
$updates = telegramApi($token, 'getUpdates', [
'timeout' => 25, // long polling: ждём до 25 сек
'offset' => $offset,
'allowed_updates' => ['message', 'callback_query'],
]);

// Смотрим, что пришло — в терминале увидите структуру массива
var_dump($updates);

Пример ответа API

Telegram вернёт JSON, который telegramApi() уже декодирует в ассоциативный массив. Структура result — массив объектов Update:

{
"ok": true,
"result": [
{
"update_id": 8473921,
"message": {
"message_id": 12,
"from": {
"id": 123456789,
"is_bot": false,
"first_name": "Ivan",
"username": "ivan_test"
},
"chat": {
"id": 123456789,
"first_name": "Ivan",
"username": "ivan_test",
"type": "private"
},
"date": 1704067200,
"text": "/start"
}
}
]
}

Что смотреть в ответе:

  • ok — true значит успех, false — ошибка (тогда смотрите error_code и description).
  • result — массив обновлений. Пустой массив [] — нормально, просто за timeout секунд ничего не пришло.
  • update_id — уникальный номер события. Обязательно сохраняйте max(update_id) + 1 как следующий offset, иначе при перезапуске получите те же события снова.
  • message — входящее сообщение. Поля: message_id, from (юзер), chat (куда пришло), date (Unix-время), text (текст). Другие типы: callback_query, inline_query, channel_post и т.д.

Для отладки удобно выводить красивый JSON в терминал:

<?php
// Вставьте после получения $updates
echo json_encode($updates, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);

Бесконечный цикл с обработкой ошибок и сохранением offset

В production скрипт должен работать вечно. Оберните вызов в try/catch, делайте экспоненциальную задержку при ошибках сети и сохраняйте offset в файл или БД. Обрабатывайте каждое обновление отдельно — ошибка в одном не должна убить остальные.

Используем функцию telegramApi() из главы Первый запрос: getMe.

<?php
// worker.php — запускается под supervisord/systemd
require_once __DIR__ . '/src/TelegramClient.php';

$token = getenv('TELEGRAM_BOT_TOKEN');
$offsetFile = __DIR__ . '/var/last_offset.txt';
$nextOffset = (int)(file_exists($offsetFile) ? file_get_contents($offsetFile) : 0);

$delay = 1;
while (true) {
try {
$updates = telegramApi($token, 'getUpdates', [
'timeout' => 25,
'offset' => $nextOffset,
'allowed_updates' => ['message', 'callback_query'],
]);

$delay = 1; // успех — сбрасываем backoff

foreach ($updates as $update) {
// Обрабатываем каждое обновление отдельно
try {
processUpdate($update, $token);
} catch (Throwable $e) {
error_log('processUpdate failed: ' . $e->getMessage());
}

// Сдвигаем offset
$nextOffset = max($nextOffset, $update['update_id'] + 1);
}

// Сохраняем offset после пачки
if ($updates) {
file_put_contents($offsetFile, (string)$nextOffset);
}

} catch (Throwable $e) {
error_log('getUpdates failed: ' . $e->getMessage());
sleep(min($delay, 30));
$delay *= 2; // экспоненциальный бэкофф: 1, 2, 4, 8... до 30 сек
}
}

function processUpdate(array $update, string $token): void {
// Для отладки: видеть каждое событие в логах
error_log('Update: ' . json_encode($update, JSON_UNESCAPED_UNICODE));

if (isset($update['message'])) {
$msg = $update['message'];
$chatId = $msg['chat']['id'];
$text = $msg['text'] ?? '';

// Эхо-ответ для теста
if ($text === '/start') {
telegramApi($token, 'sendMessage', [
'chat_id' => $chatId,
'text' => 'Бот работает в long polling!',
]);
}
}
}

Почему один процесс — один getUpdates

Bot API отдаёт каждый Update только одному вызывающему. Если запустите два скрипта с getUpdates параллельно, они будут делить события: одно придёт первому, следующее — второму. Часть обновлений потеряется для каждого воркера. Поэтому long polling подходит для одного воркера. Для горизонтального масштабирования переходите на webhook + очередь.

Где запускать воркер

  • supervisord / systemd — стандарт для PHP-демона. Автозапуск, рестарт при падении, логи в journald.
  • php -S или cron — не подходят: первый блокирует процесс, второй даёт задержку до минуты между запусками.
  • screen / tmux — только для локальной разработки. В production потеряете процесс при перезагрузке сервера.

Long polling vs Webhook

  • Long polling: не нужен HTTPS, простой запуск, легко дебажить (видите ответ в терминале). Минусы — один воркер, держит PHP-процесс, задержка до ~1 сек после таймаута.
  • Webhook: масштабируется, работает за nginx/php-fpm, нет постоянного соединения. Требует валидный SSL и публичный IP/домен.

Long polling хорош для разработки, MVP, тестов и ботов с небольшой нагрузкой (десятки сообщений в минуту). Как только появляется второй воркер или SLA выше 99% — переезжайте на webhook. Пошаговый переход — на странице «С polling на webhook».

Частые ошибки новичков

  • Потерянный offset. Сохраняйте его в файл или БД, а не только в памяти: при рестарте процесса придётся обработать ту же пачку.
  • Зависший процесс. Без try/catch исключение в processUpdate убивает цикл — ставьте обработку ошибок вокруг каждого Update.
  • Игнорирование allowed_updates. По умолчанию Telegram шлёт все типы (включая channel_post, poll и др.), что увеличивает payload. Фильтруйте: ['message', 'callback_query', 'inline_query'].
  • Слишком маленький timeout. Ставьте 20–30 секунд. При timeout=0 (по умолчанию) получаете обычный short polling — тысячи пустых запросов.

Дальше: Webhook — production-вариант с HTTPS endpoint и очередью.