Let's Encrypt Renewal Failed: The Five Failure Modes Behind Most Expired Certs
Let's Encrypt renewal failed? The common causes — broken certbot cron, blocked port 80, expired DNS API tokens, rate limits — and how to catch failures early.
"Let's Encrypt renewal failed" is one of those errors you usually discover from the wrong direction: a browser warning, a failing webhook, a client phoning the agency. The renewal had been failing for weeks — Let's Encrypt even emailed the expiry warnings to an address nobody reads — and the 90-day certificate quietly ran out. The good news is that renewal failures cluster into a handful of well-understood modes. The bad news is that every one of them fails silently unless something external is watching.
The five common failure modes
1. The certbot cron job is silently broken
The most common cause by a wide margin. Renewal worked at setup, then at some point the schedule stopped firing:
- The cron entry or systemd timer didn't survive a server migration or rebuild.
certbot.timerwas disabled during an incident and never re-enabled.- Certbot was installed via a package manager that set up a timer, then reinstalled via snap or pip, which didn't — or vice versa, leaving two half-configured mechanisms.
- The job runs, fails, writes the error to a log nobody tails, and exits. Cron doesn't care.
Check both mechanisms explicitly: systemctl list-timers | grep certbot and crontab -l (plus /etc/cron.d/). If neither shows anything, renewal isn't merely failing — it isn't being attempted.
2. Port 80 was blocked after a firewall change
HTTP-01 validation requires Let's Encrypt to reach http://yourdomain/.well-known/acme-challenge/ on port 80. Months after setup, someone hardens the firewall, "closes the insecure port", or a new security-group policy drops 80 — and validation starts failing. The site looks fine because HTTPS on 443 still works; only the renewal path is broken, and you have up to 30 days of grace before the cert expires and you find out.
Related variants: a CDN or proxy put in front of the origin that doesn't pass the challenge path through, a redirect-everything-to-HTTPS rule that breaks the challenge (usually fine — Let's Encrypt follows redirects — but not if the redirect target is wrong), or the webroot path changing during a deploy.
3. The DNS-01 API token expired or lost permissions
Wildcard certificates require DNS-01 validation, which means certbot (or acme.sh, or your cert-manager) holds an API credential for your DNS provider. These break in predictable ways: the token had an expiry date, the employee whose account issued it left, the provider rotated its API, or the token's scope was trimmed in a security review. The renewal log fills with unauthorized errors while everything else looks healthy.
4. Rate limits
Less common in steady state, but brutal when you hit them mid-incident. Let's Encrypt enforces, among others: a limit of 50 certificates per registered domain per week, 5 duplicate certificates (identical name sets) per week, and 5 failed validations per account/hostname/hour. The classic trap: renewal is failing, an engineer retries in a tight loop trying to fix it, burns through the failed-validation and duplicate limits, and now even the correct fix has to wait. Always debug with --dry-run, which uses the staging environment and doesn't count against production limits.
5. ACME account problems
Rarer, but real: the ACME account key was lost in a migration (certs are on the new server, /etc/letsencrypt/accounts isn't), the account was deactivated, or the client is so old it still speaks ACMEv1 or fails the current ACME directory's requirements. Symptoms are account does not exist or signature errors. The fix is usually re-registering and re-issuing rather than archaeology.
How to check your renewal health right now
Two commands tell you most of what you need. First, ask certbot to rehearse a renewal against staging:
# Rehearse renewal without touching production rate limits
sudo certbot renew --dry-run
# Then verify what's actually being SERVED (not what's on disk)
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -enddate
The second command matters more than people expect. A frequent failure mode is renewal succeeding — new cert written to /etc/letsencrypt/live/ — while nginx, HAProxy or Apache keeps serving the old one because the deploy hook that reloads the service is missing or broken. On-disk expiry and served expiry are two different facts; monitor the served one.
| Symptom | Likely cause | First check |
|---|---|---|
| No renewal attempts in logs at all | Cron/timer gone | systemctl list-timers, crontab -l |
Timeout during connect on HTTP-01 | Port 80 blocked | curl -I http://domain/.well-known/acme-challenge/test from outside |
unauthorized on DNS-01 | Dead API token | Re-test the token against the DNS provider's API |
too many certificates already issued | Rate limit | Stop retrying; wait out the window; use --dry-run |
| New cert on disk, old cert served | Missing reload hook | Compare disk notAfter vs the openssl probe above |
Why 90-day certs make monitoring more necessary, not less
There's a tempting line of reasoning: short-lived certificates plus automation means expiry monitoring is obsolete. The opposite is true, and the Let's Encrypt project itself has always framed 90-day lifetimes as something that forces automation — with the explicit assumption that the automation is verified.
Three reasons short lifetimes raise the stakes:
- The failure window is always close. With a one-year cert, a broken renewal pipeline gives you months of slack. With 90-day certs renewing at 60 days, a silent breakage becomes a production outage within weeks — and the industry is moving shorter still.
- More renewals, more chances to fail. Every renewal is a distributed-systems transaction across cron, the ACME server, your firewall, your DNS provider and a reload hook. Running it four or more times a year per cert multiplies the opportunities for one link to rust.
- Humans have left the loop. Nobody "remembers" 90-day renewals; that's the point. But it means there's no human habit acting as a backstop when the machinery stops. The backstop has to be monitoring.
The robust posture is simple: automate renewal and independently monitor the certificate actually served on every endpoint, with alerts far enough out (30/14/7/1 days) that a failed renewal is a calm ticket rather than a 2 a.m. page.
DomainOps does that second half for you — continuous external checks on every endpoint's served certificate, with expiry alerts to email, Slack or Pushover before a broken renewal becomes an outage. Setup takes a few minutes: get started here.