Let's Encrypt issues certificates for ninety days and renews them automatically, which is why people stop thinking about them a week after setting it up. The trouble is that auto-renewal breaks silently: nobody sees the errors, and three months later the site greets visitors with a browser warning. Since 2025 Let's Encrypt has stopped sending expiry notification emails, so the last external reminder is gone too.

Below are the reasons renewal stops working, in descending order of frequency.

1. The certificate was renewed and the service never re-read it

The most common one. certbot renew ran, the new file is on disk, and nginx or Apache keep the old one in memory — serving it to visitors until the configuration is reloaded. Formally everything is fine; in fact the site has an expired certificate.

The cure is a hook that runs after a successful renewal:

sudo certbot renew --deploy-hook "systemctl reload nginx"

This is written once into the renewal file (/etc/letsencrypt/renewal/example.com.conf, the [renewalparams] section, the renew_hook line) and works by itself from then on.

2. The web server changed and the challenge did not

The HTTP-01 challenge requires a file from /.well-known/acme-challenge/ to be served over plain HTTP. It usually gets broken like this:

  • an unconditional HTTP to HTTPS redirect was added to the config — the challenge follows the redirect and fails;
  • a location ~ /\. rule with deny all appeared, closing every directory with a dot in it, .well-known included;
  • the site root moved while certbot's settings kept the old --webroot-path;
  • country blocking or basic authentication was switched on and caught the validation server along with everyone else.

The correct order in the config: first a dedicated location for /.well-known/acme-challenge/, and only then the general redirect.

3. The timer is off

Renewal runs from a systemd timer or a cron job, and either can be switched off by accident while debugging or lost when moving the server.

systemctl list-timers | grep certbot
sudo certbot renew --dry-run

The second command performs a full renewal cycle in test mode without spending your quota. It is the only check that answers "will this renew a month from now" in advance rather than after the fact.

4. A domain no longer points here

The certificate covers five domains, one of them moved to another host, DNS changed. Validation for that domain stops passing, and certbot does not renew the whole certificate — the four working domains included. Extra domains have to be removed from the certificate, not ignored along with the message.

5. You hit the rate limits

Let's Encrypt limits the number of certificates per domain per week. That is normally the result of a "did not work, let me try again" loop while debugging. There is a staging server for experiments (--dry-run or --test-cert), and it should be used before the limit is exhausted rather than afterwards.

Check what is served, not what is stored

Every check based on files on disk has one flaw: the file can be fresh while the visitor receives the old certificate. Look from the client's side:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -dates -subject -issuer

The -servername option is mandatory: without it, on a server with several sites you get the certificate of whichever virtual host comes first, not the one you meant. In the output, notAfter is the real expiry date.

While you are there, check the chain:

echo | openssl s_client -connect example.com:443 -servername example.com -showcerts 2>/dev/null | grep -c 'BEGIN CERTIFICATE'

If there is only one certificate, the intermediate is not being served. Browsers usually forgive that configuration — they build the chain themselves — but curl, mobile applications and Java clients will fail validation. This is the classic "it opens fine for me and does not work for the customer".

How many days of warning

Thirty days is a sensible first alert: that is also how long remains when certbot starts attempting renewal, so if nothing has happened by then, the mechanism is broken. Fourteen days is the second alert, already requiring action today. Seven is an emergency.

What matters is watching every certificate at once, including the ones that did not come from Let's Encrypt: the ones bought for a year are forgotten most reliably of all, because in a year everyone has forgotten both about them and about whose mailbox the notifications go to. A single page with the domains, the dates and the days remaining closes the question entirely — as on the demo below.