Updates

Long polling

Long polling in Telegram Bot API: how to properly call getUpdates with timeout, save offset, handle errors and run a worker under supervisord.

Long polling — a mode where your PHP script keeps an open HTTP connection to Telegram and waits for new events up to 30 seconds. Unlike regular getUpdates (which returns immediately, even if empty), here the server holds the request until updates appear or a timeout expires. This saves resources: instead of hundreds of empty requests per second — one long-lived one.

Parameters for getUpdates in long polling

The key parameter is timeout (in seconds). Set it to 20–30. At timeout=0 (default) you get short polling — thousands of empty responses. Parameter allowed_updates filters event types on Telegram's side, reducing traffic.

We use the function telegramApi() which we wrote in chapter Первый запрос: getMe. There we wrote a wrapper around cURL with HTTP code checking, json_last_error and field ok — here we simply call it.

<?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);

Example API Response

Telegram will return JSON that telegramApi() already decodes into an associative array. The structure result — an array of objects 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"
}
}
]
}

What to look at in the response:

  • ok — true means success, false — error (then check error_code and description).
  • result — array of updates. An empty array [] is normal; just nothing came in for timeout seconds.
  • update_id — unique event ID. **Save this** max(update_id) + 1 as the next offset, otherwise on restart you'll receive the same events again.
  • message — incoming message. Fields: message_id, from (user), chat (where it came from), date (Unix timestamp), text (text). Other types: callback_query, inline_query, channel_post and so on.

For debugging it's convenient to output pretty JSON to the terminal:

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

Infinite Loop with Error Handling and Offset Persistence

In production the script must run forever. Wrap the call in try/catch, implement exponential backoff on network errors, and save offset to a file or database. Handle each update separately — an error in one shouldn't kill the others.

We use the function telegramApi() from chapter Первый запрос: 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!',
]);
}
}
}

Why One Process — One getUpdates

Bot API delivers each Update to only one caller. If you run two scripts with getUpdates in parallel, they'll split events: one comes first to the first, the next to the second. Some updates will be lost for each worker. Therefore long polling is suitable for a single worker. For horizontal scaling move to webhook + queue.

Where to Run the Worker

  • supervisord / systemd — standard for PHP daemon. Auto-start, restart on failure, logs in journald.
  • php -S or cron — don't work: the first blocks the process, the second introduces minute-long delays between runs.
  • screen / tmux — only for local development. In production you'll lose the process on server reboot.

Long Polling vs Webhook

  • Long polling: no HTTPS needed, simple setup, easy to debug (see response in terminal). Drawbacks: one worker, keeps a PHP process alive, delay up to ~1 second after timeout.
  • Webhook: scales, works with nginx/php-fpm, no persistent connection. Requires valid SSL and public IP/domain.

Long polling is good for development, MVPs, tests, and bots with low load (dozens of messages per minute). Once you have a second worker or require SLA > 99% — migrate to webhook. Step-by-step migration is on page «С polling на webhook».

Common Beginner Mistakes

  • Lost offset. Save it to a file or DB, not just in memory: on process restart you'll have to handle the same batch again.
  • Stuck process. Without try/catch exceptions in processUpdate kill the loop — add error handling around each Update.
  • Ignoring allowed_updates. By default Telegram sends all types (including channel_post, poll and others), increasing payload size. Filter: ['message', 'callback_query', 'inline_query'].
  • Timeout too small. Set 20–30 seconds. At timeout=0 (default) you get regular short polling — thousands of empty requests.

Next: Webhook — production variant with HTTPS endpoint and queue.

"}