← back to blog

SSL Certificate Renewal Day For A Self Hosted Webhook

Why renewal day matters more than it should

A Telegram webhook is just an HTTPS endpoint that Telegram’s servers call every time your bot has an update to receive. Telegram is strict about the HTTPS part. It won’t deliver updates to an endpoint whose certificate is invalid, expired, or issued by something it can’t verify. That’s a sane requirement. But it means the health of your bot is quietly tied to a certificate file that has an expiry date on it, and certificates don’t renew themselves unless you’ve built something that does it for you.

Most of the time nobody notices renewal day because it’s handled by a cron job or a systemd timer running in the background. The trouble starts when that automation silently stops working, and you don’t find out until updates stop arriving and you’re staring at a bot that looks alive but isn’t hearing anything.

How Telegram checks your certificate

When you call setWebhook, Telegram fetches your endpoint over HTTPS and validates the certificate chain the same way a browser would, unless you’ve explicitly uploaded a self-signed certificate as part of that call. There are two distinct paths here and they behave very differently on renewal day.

If you’re using a certificate from a public CA, Let’s Encrypt is the common choice, Telegram validates it against its own trust store every time it connects. You never told Telegram what your certificate looks like, so when your certificate renews, Telegram just sees a new valid cert the next time it connects. Nothing to notify, nothing to re-upload. This is the path you want.

If you’re using a self-signed certificate, you upload the public certificate file directly in the setWebhook call as the certificate parameter. Telegram pins to that specific file. When that certificate expires and you generate a new one, the new file is a different certificate as far as Telegram is concerned, even if it covers the same domain. You have to call setWebhook again with the new certificate attached, or Telegram keeps trusting the old, now-expired one and your webhook goes dark. This is the path that catches people out, because it turns a routine cert renewal into an application-level step that’s easy to forget.

Telegram also only accepts webhooks on ports 443, 80, 88, or 8443. If your renewal process ever changes what’s listening on those ports, or a reverse proxy in front of your app starts answering on a different port during a deploy, that’s a second, unrelated way to break the same thing on the same day.

What actually happens when it fails

Telegram doesn’t throw an error into your logs when a webhook cert goes bad. It just stops delivering updates. Behind the scenes, Telegram queues updates for a while and retries delivery, but if failures keep happening it will eventually give up trying that endpoint. From your side, the bot looks fine. The process is running, the port is open, health checks against your own load balancer pass. The only thing missing is inbound traffic from Telegram, and that’s easy to miss if you’re not specifically watching for it.

The fastest way to check is getWebhookInfo. It returns last_error_date and last_error_message, and if your certificate has expired you’ll usually see something explicit like a certificate verification failure in that message. It also returns pending_update_count, which climbing steadily is a strong sign something’s wrong even before you read the error text.

Where Let’s Encrypt renewal actually breaks

Let’s Encrypt certificates are valid for 90 days, and certbot’s default renewal job checks daily and renews anything within 30 days of expiry. On paper that’s a wide safety margin. In practice, the failures we see on real deployments aren’t about the 90-day window, they’re about the automation around it:

The systemd timer or cron job for certbot silently stops firing after a server reboot or an OS update, because it was never re-enabled or the crontab entry belonged to a user account that got removed.

Certbot renews the certificate file successfully, but the web server or app process never gets told to reload it. Nginx and most webhook frameworks cache the certificate in memory at startup. A renewed file on disk does nothing until the process that’s actually terminating TLS reloads or restarts. This is what the --deploy-hook flag in certbot is for, and it’s the single most common thing missing from a renewal setup that “worked once” and then quietly stopped working.

The renewed certificate is missing the intermediate chain. Browsers often cache the intermediate from a previous connection and forgive this, so it looks fine when you check it in a browser. Telegram’s fetcher, and tools like openssl s_client, won’t be so forgiving, and you’ll see a chain validation failure even though the leaf certificate itself is perfectly valid.

DNS or the ACME challenge fails on renewal because something changed since the certificate was first issued, a firewall rule tightened, a load balancer got swapped, a DNS record for the challenge subdomain expired. The first issuance succeeded under different network conditions than the ones you have now.

Checking your own certificate before Telegram does

You don’t need to trust that renewal worked. You can verify it directly.

openssl s_client -connect yourdomain.com:443 -servername yourdomain.com < /dev/null 2>/dev/null | openssl x509 -noout -dates -issuer

This shows the expiry date and who issued it, straight from the live connection, not from a file on disk. If the chain is incomplete, openssl s_client will also flag it in the verify output, which is worth reading even when the connection succeeds.

Pair that with a call to getWebhookInfo right after a renewal to confirm Telegram itself sees a healthy endpoint, not just that your server thinks it does.

Where a reverse proxy simplifies this

If your webhook app terminates TLS itself, every renewal touches application code and needs the app to reload its own certificate files, which is one more thing that can be implemented wrong. Putting nginx, Caddy, or a similar reverse proxy in front of the app and letting it own TLS termination means renewal is entirely the proxy’s job. The webhook application only ever talks plain HTTP on localhost. Caddy in particular handles Let’s Encrypt renewal internally without a separate cron job at all, which removes one whole category of “the timer didn’t fire” failures.

This also matters if you’re running the webhook behind any kind of IP-based access control. Telegram publishes the ranges its servers connect from, and if you’re allowlisting at the proxy layer, that’s another place a renewal-adjacent change, a proxy config reload gone wrong, can quietly break delivery even though the certificate itself is fine.

Making renewal day boring

The goal is for renewal day to be something that happens without anyone noticing, not a calendar event you dread. That means three things working together: automated renewal with a deploy hook that actually reloads the service holding the certificate, a monitoring check that alerts on certificate expiry independent of whether the renewal job reports success, and a periodic getWebhookInfo check that confirms Telegram’s own view of your endpoint matches what you expect. None of these are complicated on their own. What causes outages is usually one of them quietly existing on paper but not actually running.

If you’re setting up or auditing a self-hosted webhook and want a second pair of eyes on the TLS and proxy layer specifically, that’s the kind of thing we help with day to day at telegramvault.org.

Get new guides and videos first — join the Telegram channel.

need infra for this today?