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

Розбираємо, що автоматизувати в бізнесі та як це працює на практиці. Без спаму.