IAnswerable

Why Let's Encrypt Renewal Fails and How to Fix It

24 Nov 2025

Let’s Encrypt certs last 90 days. Renewal is supposed to be invisible — a cron job or systemd timer runs quietly in the background. When it fails, you usually find out from a browser warning or a monitoring alert, not from a log you were watching. Here are the four failures that actually happen, and the exact check for each.

1. Port 80 is blocked or occupied

HTTP-01 validation (the default for single-domain certs) requires Let’s Encrypt to reach http://yourdomain.com/.well-known/acme-challenge/... on port 80. If a firewall blocks it, or another process is squatting on the port, renewal fails.

Check what’s actually listening:

sudo ss -tlnp | grep :80

If Nginx isn’t there, or something else is, that’s your problem. Also check your firewall rules — a ufw or cloud security group change made for unrelated reasons is a common silent cause:

sudo ufw status | grep 80

The fix is to make sure port 80 stays open and pointed at whatever serves the ACME challenge path, even if you redirect everything else to HTTPS. A blanket “redirect all HTTP to HTTPS” rule that fires before the .well-known location block will break every future renewal.

2. The webroot path doesn’t match reality

If certbot uses the webroot plugin, it writes challenge files to a directory and expects your webserver to serve them from there. If you moved your site root, changed frameworks, or the deploy process now serves from a different directory, the webroot certbot writes to and the one Nginx serves from diverge.

sudo certbot renew --dry-run

The dry run output will show a 404 on the challenge URL if this is the issue. Fix it by pointing --webroot-path at the actual served directory, or switch to the Nginx or DNS plugin so certbot doesn’t depend on file serving at all.

3. DNS-01 propagation timeout (wildcard certs)

If you’re on a wildcard cert using DNS-01, renewal fails when the TXT record hasn’t propagated by the time Let’s Encrypt checks it. This is intermittent — it can work for months and then fail once because your DNS provider had a slow day.

Check the propagation delay you configured against the plugin default:

grep propagation /etc/letsencrypt/renewal/example.com.conf

If it’s unset or too low, add or increase dns_cloudflare_propagation_seconds (or your provider’s equivalent) in that renewal config file directly. You don’t need to re-run the full issuance command — editing the .conf file under /etc/letsencrypt/renewal/ is what certbot renew reads from.

4. Rate limits from repeated failed attempts

Let’s Encrypt limits how many certificates you can issue per domain per week. If something upstream is broken and your renewal cron retries aggressively — some setups retry hourly on failure — you can burn through the limit and get a hard rate-limit error that has nothing to do with your original problem.

Check your renewal frequency:

systemctl list-timers | grep certbot

The standard certbot timer runs twice a day and only actually renews certs within 30 days of expiry — that’s fine. The problem is custom cron jobs that call certbot renew more often “just to be safe.” They don’t renew any faster, since certbot checks expiry before doing anything, but a broken renewal retried too often can still hit issuance limits when it actually attempts a real request.

The fix that prevents all four

Run certbot renew --dry-run on a schedule separate from actual renewal — weekly, via a monitoring check that alerts on non-zero exit code. A dry run exercises the exact same validation path without consuming your issuance quota. You want to know about a broken renewal 60 days before expiry, not the day your cert goes red.

sudo certbot renew --dry-run && echo "OK" || echo "RENEWAL BROKEN"

Wire that into whatever already pages you — cron plus a simple healthcheck ping, or your existing monitoring stack. A cert failure you catch in week one is a five-minute fix. One you catch when the browser warning shows up is an incident.