Telegram Webhooks vs Polling: The Decision, With Failure Modes
Telegram Webhooks vs Polling: The Decision, With Failure Modes
Choosing between a Telegram webhook and polling decides what your infrastructure has to look like — and how your bot will eventually break. Every bot receives updates one of these two ways.
Long polling (getUpdates): your bot repeatedly asks Telegram for updates. Webhooks (setWebhook): Telegram sends an HTTPS POST to your server when something happens.
What webhooks actually require
This is where people underestimate the commitment. From the API documentation:
HTTPS, mandatory. The url parameter is documented as an "HTTPS URL to send updates to." There is no plaintext option.
One of exactly four ports: "Ports currently supported for webhooks: 443, 80, 88, 8443." Not 3000, not 8080. If your platform assigns arbitrary ports, you need a proxy in front.
A publicly reachable address. No localhost, no private network, no machine behind NAT without a tunnel.
A valid certificate — or a self-signed one uploaded via the certificate parameter, in which case Telegram checks against the key you supplied. The docs are specific about how: "Please upload as InputFile, sending a String will not work." Post it as multipart/form-data in PEM (ASCII BASE64) format, and make sure the certificate's CN or SAN matches the domain in your webhook URL — for a self-signed certificate the IP is acceptable as CN.
All of the above describes the hosted API at api.telegram.org. Running your own Local Bot API Server lifts three of these constraints and raises a fourth limit — the docs list "Use an HTTP URL for the webhook.", "Use any local IP address for the webhook.", "Use any port for the webhook." and "Set max_webhook_connections up to 100000." That is why you will occasionally see people claim plaintext webhooks work.
Useful optional parameters most people never set:
secret_token— Telegram includes it in anX-Telegram-Bot-Api-Secret-Tokenheader so you can verify requests genuinely came from Telegram. Set this. Your webhook URL is otherwise an open endpoint anyone can POST to.max_connections— simultaneous connections, 1–100, default 40. Lower it to protect a small server; raise it for throughput.allowed_updates— receive only the update types you handle. Less traffic, less parsing. Two traps:chat_member,message_reactionandmessage_reaction_countare excluded unless you list them explicitly, and "If not specified, the previous setting will be used" — omitting the parameter inherits your last subscription instead of resetting it. Pass it explicitly every time.ip_address— pin a fixed IP instead of resolving DNS.
What polling requires
A process with outbound internet access — and no webhook set on the token. The two are mutually exclusive: getUpdates "will not work if an outgoing webhook is set up." If you have ever called setWebhook on this token, call deleteWebhook first; getWebhookInfo returns an empty url field when no webhook is set.
getUpdates takes offset, limit (1–100, default 100) and timeout. One note from the docs that matters: timeout "Defaults to 0, i.e. usual short polling. Should be positive, short polling should be used for testing purposes only."
Always set a positive timeout. Leaving it at 0 means hammering Telegram with empty requests — that is short polling, and the documentation explicitly says it is for testing. Use 30 or so for real long polling.
The failure modes
This is the part worth reading twice, because the two approaches fail in completely different ways.
Webhooks fail silently
Certificate expiry. Your TLS certificate lapses. Telegram cannot complete an HTTPS request. Updates stop arriving. Your bot process is running perfectly and reports no error — it is simply never called. Monitoring that checks whether your bot is alive will show green while your bot receives nothing.
This is one of the failure modes we see most often, and it is invisible to every naive health check. Certificates expire on a schedule; if renewal breaks quietly, you find out when a user complains.
Any non-2XX response. The documentation says: "In case of an unsuccessful request (a request with response HTTP status code different from 2XY), we will repeat the request and give up after a reasonable amount of attempts."
"A reasonable amount of attempts" is not defined, and neither is the retry interval — so you cannot compute how long an outage has to last before Telegram stops trying. What the docs do say is that undelivered updates are "stored on the server until the bot receives them either way, but they will not be kept longer than 24 hours", and that getWebhookInfo reports pending_update_count, the "Number of updates awaiting delivery". Whether an update Telegram has stopped retrying is still counted there is not documented — we don't know. Assume a long outage costs you updates; do not assume a brief 502 window does.
Slow responses. If your handler does the work before responding, Telegram waits. Respond 200 immediately, process asynchronously.
Silent misconfiguration. Wrong port, expired DNS, a proxy stripping the path — none of these raise an error in your process; you just see no updates. Telegram does record them: getWebhookInfo returns last_error_date and last_error_message, "Error message in human-readable format for the most recent error that happened when trying to deliver an update via webhook." The failure is invisible only if nobody reads that endpoint.
Polling fails loudly
Your process dies. Obvious: it stops, alerts fire, you restart it. Updates queue at Telegram and arrive when you come back — for up to 24 hours. Retention is not indefinite. The docs say incoming updates "will not be kept longer than 24 hours" — for webhooks and polling alike. An outage longer than a day loses updates whichever transport you run.
Network loss. Your requests fail visibly in your own logs.
Conflicting instances. Two processes polling the same bot fight over updates. This one is genuinely confusing to debug — symptoms are intermittent missed messages — but it shows up as errors rather than silence.
The pattern: polling failures announce themselves; webhook failures are silence that looks like calm.
The comparison
| Webhooks | Long polling | |
|---|---|---|
| Infrastructure | HTTPS, public, 4 ports | Outbound internet only |
| Latency | Lowest | Slightly higher |
| Efficiency at scale | Better | Wasteful at high volume |
| Behind NAT / local dev | No (needs a tunnel) | Yes |
| Failure visibility | Silent | Loud |
| Certificate management | Required, ongoing | None |
| Lost updates on outage | Possible | Queued — but only for 24 hours |
| Setup complexity | Higher | Trivial |
Choosing
Use polling when:
- You are developing or testing
- Your bot is small to medium volume
- You do not want to run TLS
- You are behind NAT, on a home server, or on a VPN
- Reliability matters more than milliseconds — updates queue for up to 24 hours instead of being dropped immediately
Use webhooks when:
- Volume is high enough that polling wastes real resources — see what hosting actually costs
- You need the lowest latency
- You already run HTTPS infrastructure properly
- And you have certificate expiry monitoring
That last condition is not optional. If you cannot guarantee your certificate will never silently expire, polling is the more reliable choice regardless of what is theoretically more efficient.
If you run webhooks, monitor these
- Certificate expiry, with an alert weeks ahead — not at expiry
- Update arrival rate — alert when it drops to zero, since zero is what failure looks like
getWebhookInfo— it reports pending update count and last error; poll it periodically- Non-2XX rate on your endpoint
- End-to-end: a canary that actually messages the bot and confirms a reply
Numbers two and five are the ones that catch the certificate case, because they measure delivery rather than liveness — and number three names the cause, since last_error_message reports the failure in human-readable form. Number one is the only one that catches it before users do. Detecting bot downtime works through each of these in detail.
The pragmatic recommendation
Start with polling. It is simpler, and it fails visibly: the outage shows up in your own process rather than only in getWebhookInfo. Retention is the same either way — up to 24 hours. It is also what makes free hosting workable while you are still proving the idea. Move to webhooks when volume justifies it and when you have the operational maturity to monitor a silent failure mode — and remember the switch is not additive in either direction: setWebhook stops getUpdates working, and you must call deleteWebhook before going back.
Plenty of production bots serving large audiences run long polling successfully. The efficiency argument for webhooks only becomes compelling at scale — and the reliability argument runs the other way until your monitoring is genuinely good.
Parameters and constraints quoted from Telegram's Bot API documentation, verified September 2026.