How to make sure your HTTPS certificates renew, and find out before they expire

Certbot renews Let's Encrypt certificates on its own. But when renewal fails, nobody tells you, and one day visitors see a big red warning. Here is how to check it, fix the usual problems, and get a warning in time.

Nobody will email you anymore. Let's Encrypt stopped its "your certificate expires soon" emails in June 2025, and certificates get shorter from February 2027. Add your own check (step 7). It takes ten minutes.

Before you start

  • This guide is for a server that already has HTTPS from certbot. If yours doesn't yet, start with our guide to HTTPS with Let's Encrypt.
  • The examples use example.com and Nginx. Where Apache is different, we say so.
  • We tested every step on Ubuntu 24.04 (certbot 2.9) and Debian 13 (certbot 4.0) with Nginx, and on Debian 13 with Apache, against a private test version of Let's Encrypt.

1. See how renewal runs

The certbot package from Ubuntu or Debian adds a systemd timer. It runs certbot renew twice a day, at a random time. Each run renews only the certificates that are close to their end date, so running it often is fine. Check that it is on:

systemctl list-timers 'certbot*' 'snap.certbot*'

You should see certbot.timer with a time in the NEXT column. If you installed certbot as a snap instead, the timer is called snap.certbot.renew.timer. If the list is empty, turn the timer on:

sudo systemctl enable --now certbot.timer

The package also adds /etc/cron.d/certbot. Don't worry about it: it only runs on servers without systemd, so renewal doesn't run twice. You don't need a cron job of your own.

To see what the last runs did:

sudo journalctl -u certbot.service --since "-7 days"

A good run prints almost nothing. A failed run shows Failed to renew certificate and Failed to start certbot.service. Certbot also keeps a detailed log in /var/log/letsencrypt/letsencrypt.log.

When does certbot renew? Certbot 2.9 (Ubuntu 24.04) renews when 30 days are left. Certbot 4.0 and newer (Debian 13) renews when a third of the certificate's life is left, so about 30 days for today's 90-day certificates. Newer certbot versions (4.1 and up) can also ask Let's Encrypt when to renew.

2. See your certificates

sudo certbot certificates

For each certificate you get the names it covers, the end date and the days left:

  Certificate Name: example.com
    Domains: example.com www.example.com
    Expiry Date: 2027-01-14 09:12:40+00:00 (VALID: 63 days)
    Certificate Path: /etc/letsencrypt/live/example.com/fullchain.pem

If a certificate has fewer than 30 days left, renewal should already have happened. Look at the journal from step 1 to see why it didn't. A certificate for a site you no longer run will keep failing. Remove it with sudo certbot delete --cert-name old.example.com.

3. Do a practice renewal

sudo certbot renew --dry-run

This asks Let's Encrypt's test server for new certificates and throws them away. It doesn't use up your real limits and changes nothing. You want:

Congratulations, all simulated renewals succeeded:
  /etc/letsencrypt/live/example.com/fullchain.pem (success)

One catch: a practice run skips the step that loads the new certificate into Nginx or Apache. The log says Dry run: skipping deploy hook command. So a good dry run doesn't prove your visitors get the new certificate. Step 4 checks that.

4. Make sure Nginx or Apache loads the new certificate

Nginx and Apache read the certificate file only when they start or reload. If nothing reloads them after a renewal, they keep serving the old certificate from memory, until it expires. Certbot says "success", and visitors still get an error.

See how each certificate was set up:

sudo grep -H -E '^(authenticator|installer)' /etc/letsencrypt/renewal/*.conf
  • installer = nginx or installer = apache: you used certbot --nginx or certbot --apache. Certbot reloads the web server after each renewal and says Reloading nginx server after certificate renewal. Nothing to do.
  • No installer line (for example authenticator = webroot or standalone): you used certbot certonly. Nothing reloads the web server. In our test, the renewed file on disk was new, but Nginx kept serving the old certificate.

For the second case, add a deploy hook: a small script certbot runs after every successful renewal. Save it as /etc/letsencrypt/renewal-hooks/deploy/reload-web-server:

#!/bin/sh
# Runs after each successful renewal: load the new certificate.
if systemctl is-active --quiet nginx; then systemctl reload nginx; fi
if systemctl is-active --quiet apache2; then systemctl reload apache2; fi

Make it runnable, and run it once to check it works:

sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/reload-web-server
sudo /etc/letsencrypt/renewal-hooks/deploy/reload-web-server

Every script in that folder runs for every certificate, so you set it up once. For a new certificate you can also give the command when you get it, and certbot remembers it:

sudo certbot certonly --webroot -w /var/www/example -d example.com --deploy-hook "systemctl reload nginx"

Check what visitors get. Compare the certificate the server sends with the one on disk:

echo | openssl s_client -connect 127.0.0.1:443 -servername example.com 2>/dev/null | openssl x509 -noout -serial -enddate
sudo openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem -noout -serial -enddate

Both lines should show the same serial=. If the first one is older, the web server still has the old certificate. Reload it and add the hook.

5. Fix the usual reasons renewal fails

Let's Encrypt checks that you control the domain by fetching a small file from http://example.com/.well-known/acme-challenge/, over port 80. Most failures are something blocking that. Below are the errors we got in our tests. The Detail: line from the real Let's Encrypt is worded a little differently, but it names the same problem and the same status code.

Port 80 is blocked

  Type:   connection
  Detail: ... Timeout ...
Hint: The Certificate Authority failed to verify the temporary Apache configuration changes made by Certbot. Ensure that the listed domains point to this Apache server and that it is accessible from the internet.

A firewall drops the request. It is easy to close port 80 later, for example when you "only need HTTPS". Let's Encrypt can only check over port 80. Open it again on the server (sudo ufw allow 80/tcp, see our UFW guide) and in your hosting provider's firewall. Port 80 can simply send everyone to HTTPS: Let's Encrypt follows that redirect.

Behind Cloudflare? If you let only Cloudflare reach your server, renewal works as long as the DNS record is proxied (orange cloud), because Let's Encrypt then comes in through Cloudflare. Switch the record to "DNS only" and Let's Encrypt connects straight to your server, gets blocked, and you see the same timeout. Keep the record proxied, or use the DNS check with Cloudflare's certbot plugin instead.

The domain now points to another server

  Type:   unauthorized
  Detail: ... /.well-known/acme-challenge/... returned 404
Hint: The Certificate Authority failed to download the temporary challenge files created by Certbot. Ensure that the listed domains serve their content from the provided --webroot-path/-w and that files created there can be downloaded from the internet.

You moved the site, but the old server still tries to renew. Let's Encrypt asks the new server for a file that only exists on the old one. Check where the name points with dig +short example.com. Renew on the server the domain points to now, and remove the certificate from the old server with sudo certbot delete --cert-name example.com.

A password or redirect in front of the check

  Type:   unauthorized
  Detail: ... /.well-known/acme-challenge/... returned 401

401 means a password is in the way, for example the one on a staging copy. With certbot --nginx this didn't matter in our tests, because certbot adds its own temporary rule. With certonly --webroot it failed. Let that one folder through without a password. In the Nginx server block for port 80:

location ^~ /.well-known/acme-challenge/ {
    auth_basic off;
}

Then sudo nginx -t && sudo systemctl reload nginx and run the dry run again. On Apache, allow the same folder without a password.

Redirects are fine within limits. Let's Encrypt follows up to 10 of them, only to http:// or https:// on ports 80 or 443. A redirect to a login page, or to another server that doesn't have the file, makes the check fail.

The site's folder changed

With --webroot, certbot remembers the folder from the first time. If you later moved the site, say from /var/www/example to /var/www/example/public, certbot puts the check file in the old folder and you get the same 404 as above. Tell certbot the new folder:

sudo certbot reconfigure --cert-name example.com --webroot -w /var/www/example/public

It does a practice run first and then says Successfully updated configuration.

Don't keep retrying for real. Let's Encrypt limits failed attempts. Fix the problem, then test with --dry-run. Avoid --force-renewal unless you need a new certificate today.

6. Why this matters more from 2027

The dates, from Let's Encrypt's own announcements:

  • 4 June 2025: the expiry warning emails ended. Let's Encrypt suggests using a monitoring service instead.
  • 10 February 2027: new certificates last 64 days instead of 90.
  • 16 February 2028: new certificates last 45 days.
  • Let's Encrypt will also re-check your domain more often: a successful check is reused for 10 days from February 2027, and only for 7 hours from February 2028. So nearly every renewal will need port 80 to work, as in step 5.

The certbot timer copes with this on its own, because it checks twice a day. What breaks is anything fixed: renewing by hand, a cron job that runs every 60 days, or a renew_before_expiry line you once added. Look for one:

sudo grep -r renew_before_expiry /etc/letsencrypt/renewal/

And shorter certificates leave less time to notice a problem. That is what the next step is for.

7. Get a warning before a certificate expires

This small script asks your web server for each site's certificate and prints a warning when fewer than 14 days are left. It checks the certificate the server really sends, so it also catches a missing reload from step 4. Save it as /usr/local/bin/cert-alert:

#!/bin/sh
# Warn when an HTTPS certificate expires in less than DAYS days.
DAYS=14
SITES="example.com www.example.com"
MAILTO="you@example.com"

warnings=""
for site in $SITES; do
  cert=$(echo | timeout 15 openssl s_client -connect 127.0.0.1:443 -servername "$site" 2>/dev/null)
  if ! printf "%s\n" "$cert" | openssl x509 -noout 2>/dev/null; then
    warnings="$warnings
$site: could not read the certificate"
  elif ! printf "%s\n" "$cert" | openssl x509 -noout -checkend $((DAYS * 86400)) >/dev/null; then
    end=$(printf "%s\n" "$cert" | openssl x509 -noout -enddate | cut -d= -f2)
    warnings="$warnings
$site: certificate expires $end"
  fi
done

if [ -n "$warnings" ]; then
  echo "HTTPS certificate warning on $(hostname):$warnings"
  if command -v mail >/dev/null; then
    echo "$warnings" | mail -s "HTTPS certificate warning on $(hostname)" "$MAILTO"
  fi
  exit 1
fi
echo "All certificates are good for at least $DAYS days."

Change SITES and MAILTO to yours. It connects to 127.0.0.1, so it also works behind Cloudflare. If your web server listens only on its public address, put that address there instead. Make it runnable and try it:

sudo chmod 755 /usr/local/bin/cert-alert
/usr/local/bin/cert-alert

To see what a warning looks like, run it once with a limit of 90 days:

sed 's/^DAYS=14/DAYS=90/' /usr/local/bin/cert-alert | sh
HTTPS certificate warning on web1:
example.com: certificate expires Jan 14 09:12:40 2027 GMT

Run it every day with a systemd timer. Make /etc/systemd/system/cert-alert.service:

[Unit]
Description=Check HTTPS certificate expiry dates

[Service]
Type=oneshot
ExecStart=/usr/local/bin/cert-alert

And /etc/systemd/system/cert-alert.timer:

[Unit]
Description=Check HTTPS certificates every day

[Timer]
OnCalendar=daily
RandomizedDelaySec=1h
Persistent=true

[Install]
WantedBy=timers.target

Turn it on and run it once by hand:

sudo systemctl daemon-reload
sudo systemctl enable --now cert-alert.timer
sudo systemctl start cert-alert.service
systemctl list-timers cert-alert.timer

Warnings go to the journal (sudo journalctl -u cert-alert), and the service shows as failed in systemctl --failed. The script also emails you, but only if the server can send email (the mail command, from the mailutils package, plus a working mail setup). As in our disk space guide, you can change the mail line to send the text to a chat or alert service you already use, for example with curl.

Why 14 days? Certbot starts renewing at about 30 days left (about 21 with 64-day certificates on certbot 4.0) and tries twice a day. Under 14 days means it has been failing for a week or more, with time left to fix it. With 45-day certificates in 2028, certbot 4.0 renews at 15 days left, so lower the limit to 7 then.

Check from outside too. The script won't help if the whole server is down. Our free site checker tells you if HTTPS fails from the outside, for example an expired or wrong certificate. It doesn't count the days left, so an outside monitoring service that warns you about expiry is worth having as well.

If something goes wrong

  • The timer list from step 1 is empty: run sudo systemctl enable --now certbot.timer. On a server without systemd, check that /etc/cron.d/certbot is there and cron is running.
  • The dry run works, but visitors see an expired certificate: the web server wasn't reloaded. Reload it now and add the deploy hook from step 4.
  • "too many failed authorizations" or "too many certificates": you hit a Let's Encrypt limit. Wait, fix the cause with --dry-run, then let the timer try again.
  • The script says "could not read the certificate": the name in SITES has no HTTPS site on this server, or the web server is down. Check with sudo nginx -t or sudo apache2ctl configtest and systemctl status nginx.
  • You need more details: run sudo certbot renew --dry-run -v and read /var/log/letsencrypt/letsencrypt.log.

Official sources

Next steps: if you haven't yet, add security headers on Nginx or Apache, and work through the security checklist for a new PHP server.

ServerNest is being built to get and renew a free certificate for each site, with a check that renewal is running. The product is still in development. Join early access to try it first.

← Back to the security blog