DEV Community

unifyport for UnifyPort

Posted on Originally published at unifyport.ai

Telegram getUpdates vs setWebhook: A Safe Migration Runbook

Your Telegram bot worked yesterday.

After a deployment, one of these things happens:

  • getUpdates returns no messages;
  • the webhook endpoint receives nothing;
  • old updates suddenly arrive in a flood;
  • the application processes some messages twice;
  • both teams insist their receiver is configured correctly.

The underlying rule is simple:

Telegram Bot API polling with getUpdates and push delivery with setWebhook are mutually exclusive for the same bot.

They are two alternative delivery modes—not two layers that should run in parallel.

A safe migration therefore requires four things:

Inspect the current mode
        ↓
Stop the current receiver
        ↓
Choose what happens to pending updates
        ↓
Start and verify the new receiver
Enter fullscreen mode Exit fullscreen mode

This runbook covers both migration directions.

The two receiving modes

Telegram bots can receive updates through polling or webhooks.

Long polling with getUpdates

Your application repeatedly asks Telegram for new updates:

Application → Telegram: Do you have updates?
Telegram → Application: Here are the updates
Enter fullscreen mode Exit fullscreen mode

Example:

curl \
  "https://api.telegram.org/bot$BOT_TOKEN/getUpdates?timeout=30"
Enter fullscreen mode Exit fullscreen mode

Polling is often convenient for:

  • local development;
  • small bot deployments;
  • workers without a public HTTPS endpoint;
  • simple single-process applications.

Your worker owns the delivery loop and must advance the update offset correctly.

Push delivery with setWebhook

Telegram sends each update to your HTTPS endpoint:

Telegram → Application: Here is an update
Application → Telegram: HTTP 2xx
Enter fullscreen mode Exit fullscreen mode

Example:

curl -X POST \
  "https://api.telegram.org/bot$BOT_TOKEN/setWebhook" \
  -d "url=https://support.example.com/telegram/bot-webhook"
Enter fullscreen mode Exit fullscreen mode

Webhooks are often a better fit for:

  • production HTTP services;
  • horizontally scalable consumers;
  • low-latency processing;
  • infrastructure that already receives external events.

The endpoint should acknowledge requests quickly and move slow work to a queue.

Why the conflict happens

When a webhook is configured, Telegram does not allow the same bot to receive updates through getUpdates.

A polling worker may still be running, but it is no longer the active Telegram delivery path.

This creates a confusing operational state:

Polling process: healthy and still running
Telegram webhook: configured
getUpdates: unable to receive updates
Enter fullscreen mode Exit fullscreen mode

From an infrastructure dashboard, the polling worker looks healthy. From the bot’s perspective, it is no longer receiving anything.

The reverse migration can also fail when a team deploys a webhook receiver but forgets to stop or retire the previous polling deployment.

Even when Telegram itself has one active delivery mode, multiple application instances may still compete inside your infrastructure and create duplicate downstream processing.

First step: inspect the active mode

Before changing a deployment flag, call getWebhookInfo:

curl \
  "https://api.telegram.org/bot$BOT_TOKEN/getWebhookInfo"
Enter fullscreen mode Exit fullscreen mode

Keep BOT_TOKEN in an environment variable. Do not paste it into shell history, tickets, screenshots or application logs.

The most important field for this diagnosis is url.

url is non-empty → a Telegram Bot API webhook is configured
url is empty     → no Bot API webhook is configured
Enter fullscreen mode Exit fullscreen mode

Do not guess the active mode from:

  • which service is currently deployed;
  • which process is running;
  • what the environment variable says;
  • what the previous release was supposed to do.

Telegram’s current webhook configuration is the state that matters.

Diagnose common symptoms

Symptom Likely explanation First check
getUpdates stopped returning messages A webhook is still configured Call getWebhookInfo
Webhook receives nothing Webhook was not set correctly or points to the wrong endpoint Inspect getWebhookInfo
Old updates arrive after switching Pending updates were retained Review the backlog policy
Updates are processed twice internally Multiple application consumers or non-idempotent storage Check worker ownership and update IDs
Test messages disappear Pending updates may have been dropped during cutover Check the migration command and logs

Treat the receiving mode and your internal processing topology as separate problems.

Telegram may have only one active delivery mode while your backend still has two consumers reading the same internal queue.

Migration rule: one receiver owns the bot

Before switching, define one owner for the bot:

telegram-receiver-mode = polling
Enter fullscreen mode Exit fullscreen mode

or:

telegram-receiver-mode = webhook
Enter fullscreen mode Exit fullscreen mode

Do not leave both production deployments enabled and rely on timing.

A useful deployment invariant is:

At every point in the migration, exactly one system is designated to process new Telegram updates.

A brief period with no active application receiver is safer than an uncontrolled period in which multiple consumers can create duplicate side effects.

Pending updates must then be handled according to an explicit policy.

Switching from webhook to getUpdates

Use this direction when returning to polling.

Step 1: pause downstream changes

Temporarily prevent bot updates from triggering irreversible side effects during the cutover.

Examples include:

  • sending duplicate replies;
  • creating duplicate support tickets;
  • charging a customer twice;
  • applying the same moderation action twice.

Your update processing should already be idempotent, but the migration is a good time to verify that assumption.

Step 2: inspect the webhook

curl \
  "https://api.telegram.org/bot$BOT_TOKEN/getWebhookInfo"
Enter fullscreen mode Exit fullscreen mode

Confirm which URL is configured before deleting it.

This avoids removing an unexpected production receiver based on a stale deployment assumption.

Step 3: choose the pending-update policy

Remove the webhook using deleteWebhook:

curl -X POST \
  "https://api.telegram.org/bot$BOT_TOKEN/deleteWebhook" \
  -d "drop_pending_updates=false"
Enter fullscreen mode Exit fullscreen mode

The critical parameter is:

drop_pending_updates
Enter fullscreen mode Exit fullscreen mode

Use false when pending messages still matter.

Use true only when the backlog can be discarded deliberately.

Situation Suggested choice
Production support messages false
Customer orders or requests false
Moderation events Usually false
Disposable test traffic Possibly true
Corrupted or unsafe backlog Make an explicit incident decision
Clean non-production reset Possibly true

Do not use true merely because it makes the migration easier.

Dropping pending updates is a business-data decision, not just a technical cleanup option.

Step 4: verify webhook removal

Call getWebhookInfo again:

curl \
  "https://api.telegram.org/bot$BOT_TOKEN/getWebhookInfo"
Enter fullscreen mode Exit fullscreen mode

Confirm that the webhook url is empty.

Do this before starting the polling worker.

Step 5: start exactly one polling worker

curl \
  "https://api.telegram.org/bot$BOT_TOKEN/getUpdates?timeout=30"
Enter fullscreen mode Exit fullscreen mode

In production, only one logical polling owner should fetch updates for that bot.

If several application replicas all start polling independently, your deployment becomes difficult to reason about even though no webhook is configured.

Step 6: advance the offset

After processing a response, advance offset beyond the highest processed update_id.

A simplified worker looks like this:

let offset = 0;

async function pollTelegram() {
  while (true) {
    const query = new URLSearchParams({
      timeout: "30",
      offset: String(offset),
    });

    const response = await fetch(
      `https://api.telegram.org/bot${process.env.BOT_TOKEN}/getUpdates?${query}`,
    );

    if (!response.ok) {
      throw new Error(`getUpdates failed: ${response.status}`);
    }

    const body = await response.json();

    for (const update of body.result) {
      await processUpdateIdempotently(update);
      offset = Math.max(offset, update.update_id + 1);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

For a durable production implementation, do not keep the confirmed offset only in process memory.

If the worker restarts after performing a side effect but before persisting progress, the same update may be processed again.

Store update IDs and processing state durably.

Switching from getUpdates to setWebhook

Use this direction when moving from polling to push delivery.

Step 1: stop the polling worker

Stop or scale down the polling deployment first.

Confirm that:

  • no polling process is still running;
  • no scheduled job calls getUpdates;
  • no developer machine uses the production bot token;
  • no old deployment remains active in another region.

Stopping the visible worker is not enough if another automation still polls the same bot.

Step 2: finish or checkpoint current work

Allow in-flight updates to complete or store them durably before terminating the worker.

The polling application should record which update_id values were accepted so the webhook receiver can continue processing idempotently.

Step 3: deploy the webhook endpoint

The endpoint should be ready before you register it with Telegram.

A minimal Express receiver might look like this:

app.post("/telegram/bot-webhook", async (req, res) => {
  const update = req.body;

  const inserted = await storeUpdateIfAbsent({
    updateId: update.update_id,
    payload: update,
  });

  res.status(200).end();

  if (inserted) {
    await enqueueTelegramUpdate(update.update_id);
  }
});
Enter fullscreen mode Exit fullscreen mode

In a production implementation, separate the HTTP acknowledgment from slow CRM, database-enrichment or AI operations.

The reliable pattern is:

Receive
  ↓
Validate
  ↓
Store idempotently
  ↓
Return success
  ↓
Process asynchronously
Enter fullscreen mode Exit fullscreen mode

Step 4: configure the webhook

curl -X POST \
  "https://api.telegram.org/bot$BOT_TOKEN/setWebhook" \
  -d "url=https://support.example.com/telegram/bot-webhook"
Enter fullscreen mode Exit fullscreen mode

Use a production HTTPS endpoint controlled by your team.

Do not point a production bot at:

  • a developer laptop;
  • a temporary tunnel with an unknown lifetime;
  • a staging endpoint;
  • an endpoint that performs slow work before responding.

Step 5: verify the configured URL

curl \
  "https://api.telegram.org/bot$BOT_TOKEN/getWebhookInfo"
Enter fullscreen mode Exit fullscreen mode

Confirm that the returned URL is exactly the endpoint you intended to configure.

This catches:

  • environment mix-ups;
  • incorrect paths;
  • staging URLs;
  • accidental trailing route differences;
  • deployment-variable mistakes.

Step 6: send a controlled test message

Send one identifiable test message to the bot.

Verify the entire path:

Telegram accepted message
        ↓
Webhook received update
        ↓
Update stored once
        ↓
Queue job created
        ↓
Downstream handler processed it once
Enter fullscreen mode Exit fullscreen mode

A successful setWebhook call alone does not prove that your application is receiving and processing updates correctly.

Handle pending updates deliberately

Telegram stores pending updates temporarily, so a migration should not be left half-finished.

Before switching, decide:

  1. Are pending messages business-critical?
  2. Can the new receiver process old updates idempotently?
  3. Is the backlog entirely disposable test traffic?
  4. Does processing old commands after deployment create risk?
  5. Who is authorized to approve dropping the backlog?

Document the decision in the deployment record:

Pending update policy: retain and drain
Approved by: support operations
Receiver before: webhook
Receiver after: polling
Cutover time: 2026-09-09T10:00:00Z
Enter fullscreen mode Exit fullscreen mode

Never record the bot token in the deployment record.

Make both modes idempotent

Regardless of the delivery method, store each Telegram update_id once.

A conceptual database table could be:

CREATE TABLE telegram_updates (
  bot_id TEXT NOT NULL,
  update_id BIGINT NOT NULL,
  payload JSONB NOT NULL,
  status TEXT NOT NULL,
  received_at TIMESTAMPTZ NOT NULL,
  processed_at TIMESTAMPTZ,
  PRIMARY KEY (bot_id, update_id)
);
Enter fullscreen mode Exit fullscreen mode

The unique key prevents a repeated update from creating another business action.

A handler can then claim stored work atomically:

async function processUpdateIdempotently(update) {
  const inserted = await insertUpdateIfAbsent(update);

  if (!inserted) {
    return;
  }

  await enqueueUpdate(update.update_id);
}
Enter fullscreen mode Exit fullscreen mode

Idempotency should be based on Telegram’s stable update identifier, not message text or arrival time.

Avoid duplicate side effects

Even with unique update storage, downstream operations may still need their own idempotency keys.

Examples include:

telegram-reply:{botId}:{updateId}
support-ticket:{botId}:{updateId}
moderation-action:{botId}:{updateId}
Enter fullscreen mode Exit fullscreen mode

If the worker crashes after creating a support ticket but before marking the update complete, a retry should find the existing ticket instead of creating a second one.

Delivery deduplication and business-operation deduplication are related but separate safeguards.

A migration checklist

Before switching

  • [ ] Confirm the bot token belongs to the expected bot.
  • [ ] Call getWebhookInfo.
  • [ ] Record the active receiving mode.
  • [ ] Identify every polling worker and webhook deployment.
  • [ ] Decide whether pending updates must be retained.
  • [ ] Verify update processing is idempotent.
  • [ ] Prepare the destination receiver.
  • [ ] Keep the bot token out of logs.

Webhook to polling

  • [ ] Call deleteWebhook.
  • [ ] Set drop_pending_updates deliberately.
  • [ ] Confirm the webhook URL is empty.
  • [ ] Start exactly one polling owner.
  • [ ] Persist processed update IDs.
  • [ ] Advance the polling offset.
  • [ ] Drain retained updates.
  • [ ] Test one new message.

Polling to webhook

  • [ ] Stop every polling worker.
  • [ ] Finish or checkpoint in-flight work.
  • [ ] Deploy the webhook receiver.
  • [ ] Configure the production webhook URL.
  • [ ] Verify it with getWebhookInfo.
  • [ ] Send a controlled test message.
  • [ ] Confirm the update was stored once.
  • [ ] Confirm downstream processing occurred once.

When the Bot API is the right model

Use Telegram’s official Bot API when:

  • users should interact with a bot identity;
  • the Telegram Update object is the right event contract;
  • bot commands are central to the product;
  • the integration is intentionally Telegram-specific;
  • polling or Telegram-native webhooks fit your infrastructure.

In that case, choose either getUpdates or setWebhook and operate it clearly.

When a unified inbound webhook fits better

The Bot API runbook solves delivery for one Telegram bot.

It does not:

  • connect an ordinary Telegram user account;
  • normalize WhatsApp, LINE, TikTok, Zalo or X;
  • create a shared cross-channel inbox contract.

If your actual requirement is:

Receive customer messages from several existing accounts
        ↓
Store them in one event model
        ↓
Route them to the same support queue
Enter fullscreen mode Exit fullscreen mode

then a normalized inbound webhook may be a cleaner architecture.

A Telegram event delivered through UnifyPort can use the same envelope as other connected providers:

{
  "id": "evt_b1a7c3e5f8",
  "type": "message.received",
  "provider": "telegram",
  "account_id": "acc_8c21d0",
  "occurred_at": "2026-09-09T12:37:00Z",
  "data": {
    "conversation": {
      "id": "5005",
      "type": "user"
    },
    "sender": {
      "id": "4004",
      "type": "user",
      "name": "Jordan Lee"
    },
    "message": {
      "id": "3003",
      "direction": "inbound",
      "sent_at": "2026-09-09T12:37:00Z",
      "text": "Can you check my order?"
    },
    "event": {
      "kind": "message_received"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Use the Bot API when the identity should be a Telegram bot.

Use an account-based inbound interface when an existing messaging account or cross-platform support queue is the real requirement.

These are different product models, not interchangeable transport settings.

Takeaway

When a Telegram bot stops receiving updates after a deployment, start with one question:

What does getWebhookInfo say?
Enter fullscreen mode Exit fullscreen mode

Then follow a controlled migration:

Inspect
  ↓
Stop the old receiver
  ↓
Choose the pending-update policy
  ↓
Start the new receiver
  ↓
Verify one end-to-end message
Enter fullscreen mode Exit fullscreen mode

Never treat getUpdates and setWebhook as parallel delivery layers.

Pick one owner, store updates idempotently, and make the pending-backlog decision explicit before switching.

References


This article was adapted from an original UnifyPort technical guide with AI-assisted editing.

Top comments (0)