Your Telegram bot worked yesterday.
After a deployment, one of these things happens:
-
getUpdatesreturns 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
getUpdatesand push delivery withsetWebhookare 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
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
Example:
curl \
"https://api.telegram.org/bot$BOT_TOKEN/getUpdates?timeout=30"
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
Example:
curl -X POST \
"https://api.telegram.org/bot$BOT_TOKEN/setWebhook" \
-d "url=https://support.example.com/telegram/bot-webhook"
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
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"
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
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
or:
telegram-receiver-mode = webhook
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"
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"
The critical parameter is:
drop_pending_updates
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"
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"
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);
}
}
}
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);
}
});
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
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"
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"
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
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:
- Are pending messages business-critical?
- Can the new receiver process old updates idempotently?
- Is the backlog entirely disposable test traffic?
- Does processing old commands after deployment create risk?
- 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
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)
);
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);
}
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}
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_updatesdeliberately. - [ ] 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
Updateobject 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
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"
}
}
}
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?
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
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
- Telegram Bot API: getUpdates
- Telegram Bot API: setWebhook
- Telegram Bot API: deleteWebhook
- Telegram webhook guide
- UnifyPort: Create a webhook endpoint
- UnifyPort: Webhook delivery and signature verification
This article was adapted from an original UnifyPort technical guide with AI-assisted editing.
Top comments (0)