The Certificate Requirement That Silently Kills Telegram Bots
The Certificate Requirement That Silently Kills Telegram Bots
There is a failure mode that takes Telegram bots offline completely while every monitor you have reports green.
The bot process is running. CPU is normal. Memory is fine. Logs show no errors — because there is nothing to log. Telegram simply stopped delivering updates, and nothing inside your own process can tell the difference between "nobody messaged me" and "I am unreachable." Telegram knows — it records the failure in getWebhookInfo — but nothing polls that by default, so the signal sits there unread.
One common cause is an expired TLS certificate — and it is the one that produces this symptom most cleanly, so it is the one this page works through.
The requirement
Webhooks require HTTPS. The url parameter of setWebhook is documented as an "HTTPS URL to send updates to." There is no plaintext option and no grace period on the hosted API at api.telegram.org. Running your own Local Bot API Server lifts this and the port restriction below — the docs list "Use an HTTP URL for the webhook." and "Use any port for the webhook." — which is why you will occasionally see people claim plaintext webhooks work.
Also enforced:
Only four ports: "Ports currently supported for webhooks: 443, 80, 88, 8443." Anything else fails.
Self-signed certificates are permitted, if you upload the public key via the certificate parameter so Telegram can verify against it. The docs are specific that it must be sent as an InputFile — "sending a String will not work."
Failures are retried, then the attempt is abandoned: "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." Note that the documented case is a request that got an HTTP status back. A TLS failure never gets that far — Telegram records it in last_error_message instead. And when Telegram stops retrying, the update is not discarded: incoming updates are "stored on the server until the bot receives them either way, but they will not be kept longer than 24 hours." They stay queued, visible as pending_update_count in getWebhookInfo, and are delivered once your endpoint is reachable again, or can be fetched with getUpdates after deleteWebhook. What you lose is latency, and anything older than 24 hours.
Why expiry is uniquely dangerous
A certificate does not degrade. It works perfectly until a precise second, then stops completely.
At that moment Telegram's HTTPS request fails. Your server never receives it, so nothing is logged. Your bot process keeps running and keeps reporting healthy. Every conventional health check — process alive, port listening, memory normal — passes.
Your bot is completely offline and every signal says it is fine.
Meanwhile users message it and get silence. Payments do not process. Support tickets are never created. /paysupport requests — which bots taking Stars payments are required to handle — go unanswered.
Nothing in that chain generates an alert, because nothing failed. Something simply stopped being called.
Why renewal breaks quietly
Automated renewal is standard and usually reliable. The failures that catch people are the ones where the automation reports success:
The certificate was never registered with the renewal system. Installed manually once, so the renewal tool has no record of it. The renewal job runs on schedule and correctly reports it has nothing to do — every day, for ninety days, right up to expiry.
Renewal succeeds but the server never reloads. New certificate on disk, old one still in memory. Files look perfect; the served certificate is expired.
Validation quietly broke. The challenge path changed, DNS moved, a firewall rule was added. Renewal starts failing months before expiry, logs the failure somewhere nobody reads.
Disk full. Renewal cannot write.
The first two are the dangerous ones, because the tooling reports success. Checking "did the renewal job run?" answers the wrong question.
Check the served certificate, not the config
The only reliable check is against what a client is actually handed, over a real connection:
echo | openssl s_client -connect yourdomain.com:443 -servername yourdomain.com 2>/dev/null \
| openssl x509 -noout -enddate
That reports the certificate genuinely being served, right now. It catches every failure above — including the renewed-but-not-reloaded case, where the file on disk is fine and the server is still presenting the old one.
Run it daily. These thresholds assume a 90-day certificate renewing at 30 days out — still the Let's Encrypt default: alert at 21 days, escalate at 10, page at 3. A 21-day warning then means something has already gone wrong and you have three weeks to fix it calmly. On shorter-lived certificates (profiles well under 90 days are increasingly common) or on ARI-driven renewal where the CA supplies the window, set the first alert below your renewal point rather than above it, or it will page on every normal renewal.
Do not build the check around "is the renewal timer enabled?" That is the check that fails.
Monitor the thing that actually stops
Certificate expiry is one cause. The general lesson is broader: monitor update arrival, not process health.
Your bot should track when it last received an update from Telegram. If that number goes to zero during hours you normally receive traffic, something between Telegram and your handler is broken — certificate, DNS, proxy, firewall, or a bad deploy. You do not need to know which to know you are down.
Also use getWebhookInfo. It reports the pending update count and the last error Telegram encountered delivering to you. Poll it every few minutes. It is the one place Telegram will tell you it has been failing to reach you, and almost nobody checks it.
A monitoring baseline
| Check | Frequency | Alert at |
|---|---|---|
| Served certificate expiry (real handshake) | Daily | 21 / 10 / 3 days |
getWebhookInfo last error | 5 minutes | last_error_date within the last 10 minutes |
getWebhookInfo pending count | 5 minutes | Growing |
| Updates received | Continuous | Zero during active hours |
| End-to-end canary message | 15 minutes | No reply |
The last one is the only check that proves the whole path works. Everything else verifies a component.
If you cannot monitor this
Use long polling instead. Call deleteWebhook first — the two are mutually exclusive, and getUpdates will not return anything while a webhook is set. Leave drop_pending_updates unset if you want the queued updates delivered.
Polling needs no certificate, no inbound HTTPS, no ports. It fails loudly — your process errors, your alerts fire. Updates queue at Telegram either way, for up to 24 hours, so the durability is the same; what changes is that the failure is visible from inside your own process instead of only in getWebhookInfo.
Webhooks are more efficient. Polling is more forgiving. If nobody on your team owns certificate monitoring, the efficient choice is the one that will eventually take you offline for two hours while every dashboard shows green.
Requirements quoted from Telegram's Bot API documentation, verified September 2026.