getUpdates long polling на PHP: offset, таймаут и защита от дублей

При разработке Telegram-ботов на локальном сервере (localhost) разворачивание полноценного HTTPS-окружения или использование внешних туннелей вроде ngrok не всегда оправдано. Метод getUpdates, работающий в режиме Long Polling, позволяет получать входящие события от Telegram API без публичного IP и настроенного SSL-сертификата. Понимание принципов работы параметров offset и timeout помогает построить надежный цикл отладки и избежать дублирования команд при повторных запусках скрипта.

Как работает Long Polling в Telegram Bot API

В отличие от классического опроса (Short Polling), когда клиент совершает частые запросы к серверу и сразу получает ответ (даже пустой), Long Polling держит HTTP-соединение открытым до тех пор, пока на стороне Telegram не появится новое событие или не истечет заданный таймаут.

Для корректной работы getUpdates критически важны два параметра:

  • timeout — время в секундах, в течение которого сервер Telegram удерживает соединение открытым при отсутствии новых входящих событий. Рекомендуемое значение — от 20 до 30 секунд.
  • offset — идентификатор первого обновления, которое вы хотите получить. Чтобы подтвердить успешное получение и обработку пакета событий, в следующий запрос передается offset = max(update_id) + 1. Это сообщает Telegram API, что все прошлые события можно удалить из очереди.

Если задать timeout = 0, скрипт превратится в ресурсоемкий Short Polling, вызывая до 100% загрузки процессора и рискуя получить от API блокировку 429 Too Many Requests.

Реализация Long Polling CLI-скрипта на PHP

Локальный скрипт обработки следует запускать через CLI (например, php worker.php). Для взаимодействия с сетевым слоем Bot API рекомендуется использовать cURL с явной проверкой HTTP-кода ответа и валидацией полученного JSON-документа.

Важный момент: таймаут cURL-запроса (CURLOPT_TIMEOUT) должен превышать параметр timeout в Bot API минимум на 5–10 секунд, иначе cURL завершит соединение раньше, чем сервер Telegram отправит ответ со статусом HTTP 200.

<?php
declare(strict_types=1);

$botToken = getenv('TELEGRAM_BOT_TOKEN') ?: '';
if ($botToken === '') {
echo "Ошибка: Не задана переменная окружения TELEGRAM_BOT_TOKEN
";
exit(1);
}

$offset = 0;
$apiTimeout = 30; // Секунд ожидания события на стороне Telegram

echo "Запуск Long Polling демона...
";

while (true) {
$url = sprintf('https://api.telegram.org/bot%s/getUpdates', $botToken);
$queryParams = [
'offset' => $offset,
'timeout' => $apiTimeout,
'allowed_updates' => json_encode(['message', 'callback_query']),
];

$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url . '?' . http_build_query($queryParams),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $apiTimeout + 10, // cURL таймаут с запасом
CURLOPT_CONNECTTIMEOUT => 5,
]);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);

if ($response === false) {
echo "cURL Error: {$curlError}. Повтор через 5 секунд...
";
sleep(5);
continue;
}

if ($httpCode !== 200) {
echo "HTTP Error {$httpCode}. Ответ: {$response}. Повтор через 5 секунд...
";
sleep(5);
continue;
}

$data = json_decode((string)$response, true);
if (json_last_error() !== JSON_ERROR_NONE || empty($data['ok'])) {
$description = $data['description'] ?? 'Некорректный JSON';
echo "API Error: {$description}. Повтор через 5 секунд...
";
sleep(5);
continue;
}

$updates = $data['result'] ?? [];
foreach ($updates as $update) {
$updateId = (int)$update['update_id'];

// Модифицируем offset, чтобы подтведить подтвержденные события
$offset = $updateId + 1;

echo "Обработка update_id: {$updateId}
";
// Здесь вызов бизнес-логики
}
}

Дедупликация и защита от повторной обработки

Смещение offset передается в Telegram только с следующим запросом getUpdates. Если ваш PHP-скрипт упадет по фатальной ошибке или будет перезапущен прямо во время выполнения цикла foreach, Telegram отправит те же самые обновления повторно при следующем запросе.

Чтобы бизнес-логика (например, создание заявки или отправка уведомления) не выполнялась дублирующим образом, необходим механизм дедупликации на уровне базы данных. Перед обработкой события сохраняйте update_id в хранилище с уникальным индексом.

<?php
declare(strict_types=1);

function handleUpdate(array $update, PDO $pdo, string $botToken): void
{
$updateId = (int)$update['update_id'];

// 1. Атомарная защита от дублей в БД
$stmt = $pdo->prepare('INSERT IGNORE INTO processed_updates (update_id, created_at) VALUES (:id, NOW())');
$stmt->execute([':id' => $updateId]);

if ($stmt->rowCount() === 0) {
// Запись уже была добавлена ранее — пропускаем повторную обработку
return;
}

// 2. Разбор входящего события
if (isset($update['callback_query'])) {
$cq = $update['callback_query'];
$chatId = (int)$cq['message']['chat']['id'];
$callbackData = (string)($cq['data'] ?? '');

// Ограничение callback_data до 64 байт проверяется на этапе отправки кнопки
if (str_starts_with($callbackData, 'create_lead:')) {
// Генерация безопасного ID лида
$leadId = bin2hex(random_bytes(7));

$insertLead = $pdo->prepare('INSERT INTO leads (lead_code, chat_id, created_at) VALUES (:code, :chat_id, NOW())');
$insertLead->execute([':code' => $leadId, ':chat_id' => $chatId]);

sendTelegramResponse($botToken, $chatId, "Ваша заявка зарегистрирована! Код: " . $leadId);
}
}
}

function sendTelegramResponse(string $token, int $chatId, string $text): bool
{
$url = sprintf('https://api.telegram.org/bot%s/sendMessage', $token);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
'chat_id' => $chatId,
'text' => $text,
'parse_mode' => 'HTML',
]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);

$res = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($res === false || $httpCode !== 200) {
return false;
}

$data = json_decode((string)$res, true);
return !empty($data['ok']);
}

Long Polling vs Webhook: когда переходить на боевую схему

Для локальной отладки и внутренних консольных утилит Long Polling идеален, однако в боевом окружении (Production) он уступает схеме с Webhook по нескольким критериям.

  • Масштабируемость: Long Polling обрабатывает обновления последовательно в одном CLI-потоке. Webhook работает поверх стандартного HTTP-сервера (Nginx / PHP-FPM) и спонтанно распараллеливает входящие запросы от пользователей.
  • Время отклика: При Webhook сервер Telegram моментально отправляет POST-запрос на ваш контроллер при наступлении события. При Long Polling возможна небольшая задержка, обусловленная паузами между итерациями цикла или обработкой тяжелых синхронных задач.
  • Управление ресурсами: Постоянный скрипт Long Polling требует следить за памятью PHP (угроза утечек памяти при длительной работе), настраивать супервизоры процессов (Systemd, Supervisor) и контролировать случайные аварийные завершения.

Важное правило: Вы не можете использовать getUpdates, если для бота уже настроен Webhook. При попытке вызова API вернет ошибку 409 Conflict. Перед запуском локального скрипта необходимо удалить Webhook.

Типичные ошибки при работе с getUpdates

  1. Ошибка 409 Conflict при вызове getUpdates: Возникает, если у бота активен Webhook или запущен второй экземпляр Long Polling скрипта с тем же токеном. Чтобы сбросить Webhook, выполните GET-запрос к https://api.telegram.org/bot<TOKEN>/deleteWebhook.
  2. Отсутствие сохранения offset между запусками: Если перезапустить скрипт без сохранения текущего offset (например, в Redis или файле), он начнет вычитывать незавершенные обновления с самого начала истории Telegram-сервера.
  3. Зависание скрипта на сетевых сбоях: Если не установить жесткий CURLOPT_TIMEOUT в cURL, скрипт может навсегда зависнуть в состоянии ожидания ответа при потерях пакетов на маршрутизаторе.
  4. Нехватка памяти в длительном процессе: При обработке тысяч событий внутри одного PHP-цикла забытые переменные или логгеры в памяти вызывают Fatal error: Allowed memory size exhausted. Всегда очищайте буферы и перезапускайте процесс по счетчику итераций.

Если вы хотите доверить проектирование архитектуры, настройку отказоустойчивых вебхуков и интеграцию Telegram-бота с вашей CRM профессионалам, обратитесь в компанию BotCreator.

Новые статьи — в Telegram

Разбираем, что автоматизировать в бизнесе и как это работает на практике. Без спама.