Hardware FixRecommendedDevice not working? Your driver may be the problemCheck updates for common hardware issues.Fix DriversOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsPC HealthRecommendedCrashes, freezes, slowdowns? Check your PC nowSpot repairable issues before they interrupt work.Check PC×
Skip to content
World desk6 min

Implement Telegram Bot Long Polling in PHP for Local Development

Use PHP cURL and Telegram’s getUpdates method to run a local bot poller, process updates, and advance offsets without a public webhook endpoint.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use Telegram’s getUpdates method from a PHP CLI process to receive bot updates without exposing a public webhook endpoint. The loop below makes a long-poll request, checks the HTTP and JSON responses, processes updates, and advances the offset so confirmed updates are not returned again.

Why use long polling for local development?

Telegram offers two mutually exclusive ways to deliver bot updates: getUpdates polling and setWebhook. With polling, your local PHP process makes outbound HTTPS requests to Telegram and asks for updates. A webhook instead requires Telegram to send requests to an HTTPS URL configured for the bot, which must be reachable by Telegram. For local development without a public endpoint, polling is the direct fit. See Telegram’s Bot API documentation and Bots FAQ.

As an Amazon Associate I earn from qualifying purchases.

A bot with an active webhook cannot use getUpdates until that webhook is removed. You can inspect its status with getWebhookInfo. Webhooks have their own requirements: Telegram currently supports ports 443, 80, 88, and 8443. Polling avoids configuring an inbound HTTPS endpoint for your local development process.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Prepare the bot and PHP environment

Create the bot and protect its token

Create a bot through Telegram’s @BotFather flow, then store the token outside committed source code—for example, in an environment variable. The token appears in the Bot API endpoint path, so do not publish it or include it in logs. Telegram’s Bots FAQ covers bot setup.

Check PHP cURL availability

The example uses PHP’s cURL extension and the CLI. PHP’s documented request flow is to initialize a handle with curl_init(), set options with curl_setopt() or curl_setopt_array(), and execute it with curl_exec(). Consult the PHP cURL manual if the extension is unavailable or you need details about its options.

Write a defensive long-polling loop

Telegram’s getUpdates method receives incoming updates using long polling. Its timeout parameter is in seconds; the default of zero is short polling, which Telegram says should only be used for testing. Set a positive timeout for the waiting period, then give cURL a total timeout longer than that period so PHP does not abandon the request first. Telegram’s PHP HelloBot example uses a five-second connection timeout and a 60-second total timeout; these are sample values, not universal requirements. See the getUpdates reference and Telegram PHP HelloBot sample.

Save this as poll.php. Set the TELEGRAM_BOT_TOKEN environment variable before running it. The example uses GET query parameters, handles transport, HTTP, and JSON failures, and only advances the offset after the whole returned batch has been processed successfully.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
<?php

declare(strict_types=1);

$token = getenv('TELEGRAM_BOT_TOKEN');
if ($token === false || $token === '') {
    fwrite(STDERR, "Set TELEGRAM_BOT_TOKEN before starting the poller.n");
    exit(1);
}

// Optional: remove an existing webhook before polling.
// Run once, then remove or comment out this block.
// $deleteUrl = "https://api.telegram.org/bot{$token}/deleteWebhook";
// $deleteResponse = requestTelegram($deleteUrl);
// var_export($deleteResponse);
// exit;

$offset = 0;
$longPollSeconds = 30;
$connectTimeoutSeconds = 5;
$totalTimeoutSeconds = 40;
$running = true;

if (function_exists('pcntl_async_signals') && function_exists('pcntl_signal')) {
    pcntl_async_signals(true);
    pcntl_signal(SIGINT, static function () use (&$running): void {
        $running = false;
    });
    pcntl_signal(SIGTERM, static function () use (&$running): void {
        $running = false;
    });
}

while ($running) {
    $url = "https://api.telegram.org/bot{$token}/getUpdates?" . http_build_query([
        'offset' => $offset,
        'timeout' => $longPollSeconds,
        'limit' => 100,
    ]);

    try {
        $response = requestTelegram($url, $connectTimeoutSeconds, $totalTimeoutSeconds);
    } catch (Throwable $e) {
        fwrite(STDERR, "Telegram request failed: {$e->getMessage()}n");
        sleep(2);
        continue;
    }

    if (($response['ok'] ?? false) !== true || !isset($response['result']) || !is_array($response['result'])) {
        fwrite(STDERR, "Telegram returned an unexpected API response.n");
        sleep(2);
        continue;
    }

    $batchHighestId = null;
    $batchSucceeded = true;

    foreach ($response['result'] as $update) {
        if (!is_array($update) || !isset($update['update_id']) || !is_int($update['update_id'])) {
            fwrite(STDERR, "Skipping malformed update.n");
            $batchSucceeded = false;
            break;
        }

        try {
            processUpdate($update);
        } catch (Throwable $e) {
            fwrite(STDERR, "Update processing failed: {$e->getMessage()}n");
            $batchSucceeded = false;
            break;
        }

        $batchHighestId = max($batchHighestId ?? $update['update_id'], $update['update_id']);
    }

    // Confirm only the batch that was processed successfully.
    if ($batchSucceeded && $batchHighestId !== null) {
        $offset = $batchHighestId + 1;
    }
}

function requestTelegram(string $url, int $connectTimeout, int $totalTimeout): array
{
    $handle = curl_init($url);
    if ($handle === false) {
        throw new RuntimeException('Could not initialize cURL.');
    }

    curl_setopt_array($handle, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CONNECTTIMEOUT => $connectTimeout,
        CURLOPT_TIMEOUT => $totalTimeout,
    ]);

    $body = curl_exec($handle);
    if ($body === false) {
        $message = curl_error($handle);
        curl_close($handle);
        throw new RuntimeException("cURL error: {$message}");
    }

    $status = (int) curl_getinfo($handle, CURLINFO_HTTP_CODE);
    curl_close($handle);

    if ($status < 200 || $status >= 300) {
        throw new RuntimeException("Telegram returned HTTP {$status}.");
    }

    $decoded = json_decode($body, true);
    if (!is_array($decoded)) {
        throw new RuntimeException('Telegram response was not valid JSON.');
    }

    return $decoded;
}

function processUpdate(array $update): void
{
    if (isset($update['message']) && is_array($update['message'])) {
        $message = $update['message'];
        $text = $message['text'] ?? '';
        $chatId = $message['chat']['id'] ?? null;

        if ($chatId !== null && is_string($text)) {
            // Replace this with your application logic.
            printf("Message in chat %s: %sn", (string) $chatId, $text);
        }
        return;
    }

    // Handle other update payload types here, such as callback_query.
    printf("Received update %d with an unhandled type.n", $update['update_id']);
}

The example’s 30-second long-poll wait and 40-second cURL total timeout are illustrative settings. Adjust them for your environment while keeping the HTTP client’s total timeout above Telegram’s requested wait. Telegram’s PHP sample demonstrates cURL response handling, including transport errors, HTTP status checks, and JSON decoding.

How the offset acknowledges updates

Each Update includes an update_id and at most one optional update payload field. In the loop, processUpdate() handles each update, and the program records the highest successfully processed ID. The next request uses that ID plus one as offset. Telegram confirms an update when a later getUpdates request uses an offset higher than its update_id; recalculating the offset after each response avoids receiving already-confirmed updates. The Bots FAQ explains that updates with IDs less than or equal to the offset are marked confirmed.

This sequence is at-least-once from the application’s perspective: if processing fails before the next offset is sent, Telegram may return that update again. Make side effects safe to retry where practical, such as by recording processed update IDs alongside durable work. The offset confirms delivery to Telegram; it is not a transaction that rolls back application work.

Run it from the CLI

On a Unix-like shell, export the token and start the process:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
export TELEGRAM_BOT_TOKEN='your-token-from-BotFather'
php poll.php

Send a message to the bot and check the terminal output. Stop the process with Ctrl+C. The optional signal handlers let the loop finish its current request and exit cleanly where PHP’s pcntl extension is available; without that extension, stop the CLI process normally and restart it when ready.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Configure update types and batch size

getUpdates supports offset, limit, timeout, and allowed_updates. The batch limit is 1–100 updates and defaults to 100. The sample explicitly requests 100, which is the API maximum. If you set allowed_updates, choose only the update types your bot needs. An empty list includes all types except chat_member, message_reaction, and message_reaction_count; omitting the parameter reuses the previous setting. Changes do not affect updates created before the call. Check the live method reference for current types and behavior; the Bot API reference showed version 10.3 dated August 24, 2026 when accessed.

Troubleshoot missing or repeated updates

getUpdates returns an error or no updates

  • Check whether a webhook is configured by calling getWebhookInfo; remove it with deleteWebhook before polling if necessary.
  • Confirm that the token is correct and that the local machine can make outbound HTTPS requests.
  • Check that the update payload you expect is included in the configured allowed_updates. A changed setting does not retroactively alter older queued updates.
  • Do not interpret an empty result as a failure by itself: a long-poll request can return successfully with no updates when none arrive during its wait.

Telegram returns the same updates again

Check that the next request uses an offset greater than every update ID you have already processed. The common pattern is to set it to the highest successfully processed update_id plus one. If your process exits before issuing that next request, Telegram has not yet received the confirmation offset and can return the updates again.

Updates seem to disappear

Telegram stores incoming updates until they are received, but no longer than 24 hours. A local poller that remains stopped beyond that retention window cannot rely on retrieving all earlier updates when it restarts.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

When to switch from polling to a webhook

Polling is convenient when the development machine can initiate HTTPS requests but does not have an inbound endpoint reachable by Telegram. For a remotely reachable deployment, a webhook can suit a server that accepts Telegram’s HTTPS requests and handles them promptly. The approaches are mutually exclusive for a bot: remove the webhook to poll, or stop polling and configure the webhook for push delivery. Telegram’s FAQ documents webhook certificate and host requirements.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
PC Slower Than It Used to Be?Free scan - under a minute
Crashes, No Sound, or Screen Glitches?Free driver scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.