Certbot renewal failed: a five-minute diagnosis

Certbot renewals fail silently — the cron ran, the log has one quiet line, and your site is days from an outage. Here are the five failure modes and the command that settles each one.

Cover Image for Certbot renewal failed: a five-minute diagnosis

You're probably here because a renewal failed, or because you just noticed the expiry date is closer than it should be. Start with the one question that settles everything else.

Step 0 — check what your server is actually serving

Not what's on disk. Not what the certbot log says. What a visitor's browser receives right now:

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

Or run our free instant check — same answer plus the chain, hostname coverage and TLS config, no signup.

If the notAfter date is comfortably in the future, your renewal is fine and something else scared you. If it's near or past, one of the five failures below is your problem. They're ordered by how often they actually bite.

1. Renewal succeeded — the service never reloaded

The most common and the most maddening. Certbot wrote a fresh certificate to disk, exited zero, logged success. But nginx, Apache, HAProxy or Postfix loads certificates at startup, and nothing told it to reload. It keeps serving the old cert until it expires, while every log on the box says you renewed weeks ago.

Compare disk to wire:

# what's on disk
openssl x509 -noout -enddate -in /etc/letsencrypt/live/yourdomain.com/fullchain.pem
# what's being served (from step 0)

If they disagree, that's it. Reload the service, then make it permanent with a deploy hook so this never depends on someone remembering:

# /etc/letsencrypt/renewal-hooks/deploy/reload.sh  (chmod +x)
#!/bin/sh
systemctl reload nginx

We wrote up a full postmortem of this exact failure — renewed on disk, old cert on the wire, nobody the wiser.

2. The challenge stopped validating

Renewal worked for months, then silently didn't. Something changed around it:

  • HTTP-01: port 80 got firewalled, or a new redirect rule sends /.well-known/acme-challenge/ somewhere else. Common after "we forced HTTPS everywhere" tickets.
  • DNS-01: someone rotated the DNS provider's API token, migrated the zone, or tightened IAM permissions. Certbot can no longer write the TXT record. One team we read about lost a subdomain's DNS record entirely — renewal failed quietly for weeks because one name on the certificate no longer resolved.

The dry run tells you which without burning a real attempt:

certbot renew --dry-run

Read the error it prints for the exact domain. Fix the challenge path, not the symptom.

3. Nothing is actually running

Certbot installs either a cron job or a systemd timer depending on distro and install method — and it's easy to end up with neither, especially after a migration or a container rebuild.

systemctl list-timers | grep -i certbot
ls /etc/cron.d/ | grep -i certbot

Both empty? There's your answer. Re-enable the timer (systemctl enable --now certbot.timer) or add the cron entry, then dry-run.

4. The errors went somewhere nobody reads

The renewal has been failing — loudly, even — but the noise went to a place no human looks. The classic, quoted verbatim from someone who lived it: the cron job was emailing its errors to root on a box with no mail server. Weeks of failure notices, delivered to nowhere.

grep -i "fail\|error" /var/log/letsencrypt/letsencrypt.log | tail -20
sudo mail  # you might be surprised

If your alerting depends on local mail, a log nobody tails, or a Slack webhook that got rotated, you don't have alerting. You have a diary.

5. Rate limits

Let's Encrypt caps issuance per registered domain per week. A re-issue loop in a pipeline, a burst of new subdomains, or repeated failed-then-retried renewals can exhaust it — and then a legitimate renewal gets refused at the worst moment. The log says too many certificates already issued. The fix is to find the loop, not to retry harder; the limit resets on a rolling window.

The structural fix

Notice what all five have in common: the failure was invisible from inside the box. The cron exited, the log line was quiet, the mail went to nowhere. Every one of them is obvious from the outside — one daily check of the certificate your server actually serves would have caught each of these with weeks to spare.

That check is free to build yourself: the step-0 one-liner in a cron on a different machine, alerting somewhere humans actually look. That's genuinely fine at small scale.

If you'd rather not own another cron job, CertPost watches the served certificate — chain, hostname and expiry — and emails you before it matters. Three certificates free, no card. Either way: watch the wire, not the log.