Let's Encrypt Certificate Renewal Failed: Fix Guide

Let's Encrypt certificates are valid for 90 days, and Certbot is designed to renew them automatically 30 days before expiry — around the 60-day mark. When renewal fails silently, the first sign is usually a browser warning that your site's certificate has expired, sometimes days after the actual failure. By then visitors have already seen the scary "Not Secure" warning, so catching renewal problems early is essential.

Renewal uses the ACME protocol, which proves domain control by either serving a challenge file over HTTP on port 80 (webroot or standalone mode) or via DNS. The most common renewal failures stem from four causes: port 80 is occupied by a running web server that is not configured to serve the challenge, the DNS A record no longer points to the server, the webroot path changed since the original issuance, or a rate limit was hit after repeated failed attempts. The five steps below cover diagnosis through to a self-healing renewal setup.

Advertisement

Step 1: Check the Renewal Error Logs

Start by reproducing the failure with a dry run, which simulates renewal without hitting Let's Encrypt's rate limits:

sudo certbot renew --dry-run

The output names the failing certificate and the specific error. For fuller detail, read the log directory:

sudo ls -lt /var/log/letsencrypt/
sudo tail -80 /var/log/letsencrypt/letsencrypt.log

Common errors you will see: "Connection refused" (port 80 unreachable), "Unauthorized" (challenge file not served), "DNS problem: NXDOMAIN" (DNS misconfigured), or "too many failed authorizations" (rate limited). The error type determines which of the next steps applies.

Step 2: Fix Port 80 Conflicts

The HTTP-01 challenge requires Let's Encrypt to fetch a file from http://your-domain/.well-known/acme-challenge/. If Nginx or Apache is already running, standalone mode cannot bind to port 80 and renewal fails with "Problem binding to port 80: Could not bind to IPv4 or IPv6."

The cleanest fix is to use webroot mode, which lets your running web server serve the challenge file. Check how the certificate was originally issued:

sudo certbot certificates

If it used standalone, switch to webroot by editing the renewal configuration file at /etc/letsencrypt/renewal/example.com.conf:

[renewalparams]
authenticator = webroot
webroot_path = /var/www/html,
[[webroot_map]]
example.com = /var/www/html

Ensure Nginx serves the challenge path from that webroot:

location ^~ /.well-known/acme-challenge/ {
    root /var/www/html;
    default_type "text/plain";
}

Step 3: Verify DNS Is Correct

The ACME challenge resolves your domain to an IP and connects to that IP on port 80. If the A record points elsewhere — for example, after a server migration or because Cloudflare's proxy is intercepting the request — renewal fails with an authorization error.

dig +short example.com

Confirm the IP matches your server. If you use Cloudflare, temporarily disable the proxy (set the record to DNS-only / grey cloud) during renewal, or use Cloudflare's own origin certificate instead. DNS propagation can also be an issue: if you changed records recently, wait for the TTL to expire before retrying.

Advertisement

Step 4: Fix Webroot or Standalone Mode Issues

If the error is "Client with the currently selected authenticator does not support any combination of challenges," the TLS-SNI challenge was deprecated and your old config needs updating. Reissue using webroot with an explicit path:

sudo certbot certonly --webroot -w /var/www/html \
  -d example.com -d www.example.com --force-renewal

For standalone mode, stop the web server first so Certbot can bind port 80:

sudo systemctl stop nginx
sudo certbot renew
sudo systemctl start nginx

This works but is disruptive, so prefer webroot mode for production. Also confirm the webroot path still exists and is writable — a common failure after restructuring a site is that /var/www/html moved but the renewal config still references it.

Step 5: Set Up Auto-Renewal and Monitoring

Certbot installs a systemd timer (or cron job) that runs renewal twice daily. Verify it is enabled:

# systemd timer
sudo systemctl list-timers | grep certbot
sudo systemctl enable --now certbot.timer

The timer runs certbot -q renew, which only acts on certificates near expiry and reloads the web server afterward via a deploy hook. Add a post-renewal hook to reload Nginx:

sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh <<'EOF'
#!/bin/bash
systemctl reload nginx
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

Finally, add monitoring so a failed renewal alerts you before expiry. The simplest approach is a check script that reports when any certificate is within 7 days of expiry, which you can wire into your existing monitoring:

sudo certbot certificates | grep "Expiry Date"

Better yet, combine a deploy hook with Uptime Robot or a health check that validates your HTTPS endpoint daily. A certificate that fails to renew should page you, not your visitors.

A common monitoring pitfall is checking only that port 443 is open, which stays true even after the certificate expires. A meaningful health check must actually validate the certificate chain and its expiry date. Tools like certbot renew --dry-run in a weekly cron job, combined with an external probe that sends an alert when the certificate has fewer than 14 days remaining, give you a comfortable buffer to fix problems before any browser shows a warning.

Quick Reference: Causes and Fixes

Symptom / Log Message Root Cause Fix
Problem binding to port 80 Web server occupies port 80 Use webroot mode; configure .well-known
Unauthorized (challenge not served) Wrong webroot path Update webroot_path in renewal config
DNS problem: NXDOMAIN A record missing or wrong Fix DNS A record; disable Cloudflare proxy
too many failed authorizations Rate limit from repeated failures Wait 1 hour; use --dry-run to test
Challenge supports no combination Deprecated TLS-SNI authenticator Reissue with --webroot
Certificate expired with no alert Timer disabled / no monitoring Enable certbot.timer; add expiry monitoring

Related Guides