Під час розробки 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
- Помилка 409 Conflict при виклику getUpdates: Виникає, якщо у бота активний Webhook або запущено другий екземпляр Long Polling скрипту з тим самим токеном. Щоб скинути Webhook, виконайте GET-запит до
https://api.telegram.org/bot<TOKEN>/deleteWebhook. - Відсутність збереження offset між запусками: Якщо перезапустити скрипт без збереження поточного
offset(наприклад, у Redis або файлі), він почне вичитувати незавершені оновлення з самого початку історії Telegram-сервера. - Зависання скрипту при мережевих збоях: Якщо не встановити жорсткий
CURLOPT_TIMEOUTу cURL, скрипт може назавжди зависнути в стані очікування відповіді при втратах пакетів на маршрутизаторі. - Брак пам'яті у тривалому процесі: При обробці тисяч подій усередині одного PHP-циклу забуті змінні або логери в пам'яті викликають
Fatal error: Allowed memory size exhausted. Завжди очищайте буфери та перезапускайте процес за лічильником ітерацій.
Якщо ви хочете довірити проектування архітектури, налаштування збійностійких вебхуків та інтеграцію Telegram-бота з вашою CRM професіоналам, зверніться до компанії BotCreator.