← Back to Blog

Fix Let's Encrypt Certificate Renewal Failed: Certbot Troubleshooting

Quick Fix

Certificate renewal failing? Run a dry run to see the exact error:

# Test renewal without making changes
sudo certbot renew --dry-run

# Check which certs need renewal
sudo certbot certificates

# Force renewal of a specific certificate
sudo certbot renew --cert-name example.com --force-renewal

Your Let's Encrypt certificate expired and your site is showing browser security warnings. Or your monitoring alerted that renewal failed silently. Let's Encrypt certificates are valid for only 90 days, and Certbot is supposed to renew them automatically. When that automation breaks, you have a narrow window to fix it before users see certificate errors. This guide covers every common renewal failure, the exact log output for each, and the fix.

Finding the Error

Before attempting fixes, get the exact error message. Certbot's logs contain the full ACME protocol exchange.

Log Locations

# Main Certbot log
sudo cat /var/log/letsencrypt/letsencrypt.log | tail -100

# Systemd timer status
systemctl status certbot.timer
journalctl -u certbot.service --since "24 hours ago"

# Renewal configuration for a specific domain
cat /etc/letsencrypt/renewal/example.com.conf

# List all managed certificates and their expiry
sudo certbot certificates

The certbot certificates output shows you exactly which certificates are about to expire:

Certificate Name: example.com
  Domains: example.com www.example.com
  Expiry Date: 2026-04-05 14:30:00+00:00 (INVALID: EXPIRED)
  Certificate Path: /etc/letsencrypt/live/example.com/fullchain.pem
  Private Key Path: /etc/letsencrypt/live/example.com/privkey.pem

Error 1: Port 80 Blocked or In Use

The Error

Attempting to renew cert (example.com) from /etc/letsencrypt/renewal/example.com.conf
Performing the following challenges:
  http-01 challenge for example.com
Cleaning up challenges
Unable to find a virtual host listening on port 80 which is
currently needed for Certbot to prove to the CA that you
control your domain.

Or with the standalone authenticator:

Problem binding to port 80: Could not bind to IPv4 or IPv6.

Why It Happens

The HTTP-01 challenge requires Let's Encrypt to make an HTTP request to port 80 on your server. If a firewall blocks port 80, or another process (nginx, apache, varnish) is already bound to it when using the standalone authenticator, the challenge fails.

The Fix

# Check what is listening on port 80
sudo ss -tlnp | grep :80

# If nginx is running and you used --standalone originally,
# switch to the nginx authenticator:
sudo certbot renew --cert-name example.com --nginx

# Or use the webroot authenticator:
sudo certbot renew --cert-name example.com \
  --webroot -w /var/www/html

# If a firewall is blocking port 80:
# AWS Security Group: add inbound rule for TCP 80 from 0.0.0.0/0
# UFW:
sudo ufw allow 80/tcp
# iptables:
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT

Important: Even if your site is HTTPS-only, port 80 must be accessible for HTTP-01 challenges. A common pattern is to redirect all HTTP traffic to HTTPS except the /.well-known/acme-challenge/ path:

# Nginx: Allow ACME challenge on port 80
server {
    listen 80;
    server_name example.com;

    location /.well-known/acme-challenge/ {
        root /var/www/html;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

Error 2: DNS Challenge Failed

The Error

Performing the following challenges:
  dns-01 challenge for example.com
Waiting for verification...
Challenge failed for domain example.com
dns-01 challenge for example.com
Cleaning up challenges
Some challenges have failed.

IMPORTANT NOTES:
 - The following errors were reported by the server:
   Type:   dns
   Detail: DNS problem: NXDOMAIN looking up TXT for
           _acme-challenge.example.com

Why It Happens

The DNS-01 challenge requires a TXT record at _acme-challenge.example.com. This fails when:

  • The DNS API credentials expired or were rotated
  • The DNS plugin is misconfigured
  • DNS propagation has not completed (TTL too high)
  • The domain's nameservers changed

The Fix

# Check if the TXT record exists
dig TXT _acme-challenge.example.com +short

# If using Cloudflare DNS plugin, verify credentials
cat /etc/letsencrypt/cloudflare.ini
# Should contain:
# dns_cloudflare_api_token = 

# Test the DNS plugin manually
sudo certbot certonly --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d example.com --dry-run

# If propagation is slow, increase the wait time
sudo certbot renew --dns-cloudflare-propagation-seconds 60

For Cloudflare, ensure the API token has Zone:DNS:Edit permission for the target zone. Scoped tokens (not global API keys) are recommended.

Error 3: Rate Limits

The Error

Error creating new order :: too many certificates (5) already issued
for this exact set of domains in the last 168 hours

Or:

Error creating new order :: too many certificates already issued for
exact set of domains: example.com

Why It Happens

Let's Encrypt enforces rate limits to prevent abuse:

  • Duplicate Certificate limit: 5 per week for the exact same set of domains
  • Certificates per Registered Domain: 50 per week
  • Failed Validation limit: 5 failures per hostname per hour
  • New Orders limit: 300 per 3 hours per account

The Fix

There is no way to bypass rate limits. You must wait for the window to reset (typically 7 days). To prevent this in the future:

# Always test with staging first
sudo certbot certonly --staging -d example.com

# Check current rate limit status
# Visit: https://tools.letsdebug.net/cert-search?m=domain&q=example.com

# Use --force-renewal sparingly (it counts against limits)
# Never put --force-renewal in a cron job

Check Your SSL Certificate

Use SecureBin's SSL Checker to verify your certificate chain, expiry date, and configuration after renewal.

Check SSL Certificate

Error 4: Expired ACME Account

The Error

Error: The client lacks sufficient authorization :: Registration
key did not match

Or:

An unexpected error occurred:
The request message was malformed :: JWS verification error

Why It Happens

The ACME account key stored locally does not match what the Let's Encrypt server expects. This happens when the account was registered on a different machine, the /etc/letsencrypt/accounts/ directory was corrupted, or the account was deactivated.

The Fix

# Re-register the account
sudo certbot register --agree-tos -m admin@example.com

# If that fails, unregister and re-register
sudo certbot unregister
sudo certbot register --agree-tos -m admin@example.com

# Or specify a new account during renewal
sudo certbot renew --cert-name example.com --register-unsafely-without-email

Error 5: Nginx or Apache Not Reloading After Renewal

The Error

Renewal succeeds but the web server keeps serving the old certificate. Browsers still show the expired certificate even though certbot certificates shows a valid certificate.

Why It Happens

Nginx and Apache load certificates into memory at startup. Renewing the certificate files on disk does not take effect until the web server reloads its configuration. If the post-renewal hook is missing or broken, the web server never picks up the new certificate.

The Fix

Add deploy hooks to the renewal configuration:

# Option 1: Add to /etc/letsencrypt/renewal/example.com.conf
[renewalparams]
# ... existing params ...

[[ post-renewal ]]
post_hook = systemctl reload nginx

# Option 2: Global hook for all certificates
# Create the hook script:
sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy
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

# Option 3: Specify during renewal
sudo certbot renew --deploy-hook "systemctl reload nginx"

For Apache:

# Replace nginx with apache2 (Debian/Ubuntu) or httpd (RHEL/CentOS)
sudo certbot renew --deploy-hook "systemctl reload apache2"

Test that the hook works:

# Dry run shows which hooks would execute
sudo certbot renew --dry-run

# Verify the certificate the web server is actually serving
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates

Error 6: Webroot Directory Mismatch

The Error

Performing the following challenges:
  http-01 challenge for example.com
Using the webroot path /var/www/html for all unmatched domains.
Waiting for verification...
Challenge failed for domain example.com
  http-01 challenge for example.com
Cleaning up challenges

IMPORTANT NOTES:
 - The following errors were reported by the server:
   Type:   unauthorized
   Detail: Invalid response from
           http://example.com/.well-known/acme-challenge/abc123:
           404

Why It Happens

The webroot path in the renewal configuration does not match where the web server actually serves files from. Common after changing the document root, migrating to a containerized setup, or using a reverse proxy that does not pass through /.well-known paths.

The Fix

# Find the actual document root
# Nginx:
grep -r "root " /etc/nginx/sites-enabled/

# Apache:
grep -r "DocumentRoot" /etc/apache2/sites-enabled/

# Update the renewal config
sudo vim /etc/letsencrypt/renewal/example.com.conf
# Change: webroot_path = /var/www/html
# To:     webroot_path = /var/www/your-actual-root

# Test it
sudo certbot renew --dry-run --cert-name example.com

If you are behind a reverse proxy (Cloudflare, AWS ALB, Varnish), the /.well-known/acme-challenge/ requests must reach Certbot. Configure the proxy to pass through this path:

# Nginx reverse proxy: pass ACME challenges to local webroot
location /.well-known/acme-challenge/ {
    root /var/www/html;
    try_files $uri =404;
}

# Everything else goes to the upstream
location / {
    proxy_pass http://backend;
}

Setting Up Reliable Automated Renewal

Systemd Timer (Recommended)

Modern Certbot packages install a systemd timer automatically. Verify it is active:

# Check timer status
systemctl status certbot.timer

# If not active, enable it
sudo systemctl enable certbot.timer
sudo systemctl start certbot.timer

# List all timers to see next run time
systemctl list-timers | grep certbot

The default timer runs twice daily at random times (to avoid thundering herd at Let's Encrypt servers). Certbot internally skips renewal if certificates are not within 30 days of expiry.

Cron Job (Fallback)

If systemd is not available or you prefer cron:

# Run twice daily with random sleep to avoid thundering herd
# Add to root crontab: sudo crontab -e
0 0,12 * * * sleep $((RANDOM \% 3600)) && certbot renew --deploy-hook "systemctl reload nginx" >> /var/log/certbot-renew.log 2>&1

Warning: Do not use both a systemd timer and a cron job. They will conflict. Check for both:

# Check for systemd timer
systemctl is-active certbot.timer

# Check for cron jobs
sudo crontab -l | grep certbot
cat /etc/cron.d/certbot 2>/dev/null

Pre and Post Hooks

Hooks run before and after renewal attempts. Common use cases:

# Pre-hook: stop a service that blocks port 80
sudo certbot renew \
  --pre-hook "systemctl stop varnish" \
  --post-hook "systemctl start varnish" \
  --deploy-hook "systemctl reload nginx"
  • --pre-hook: Runs before every renewal attempt (even if cert is not due)
  • --post-hook: Runs after every renewal attempt (even if cert is not due)
  • --deploy-hook: Runs only when a certificate is actually renewed. This is what you want for web server reloads.

Wildcard Certificate Renewal

Wildcard certificates (*.example.com) require the DNS-01 challenge. They cannot use HTTP-01. This means automated renewal requires a DNS API plugin:

# Initial issuance with Cloudflare DNS
sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d "*.example.com" -d "example.com"

# The renewal config automatically uses dns-cloudflare
# Verify:
cat /etc/letsencrypt/renewal/example.com.conf | grep authenticator
# Should show: authenticator = dns-cloudflare

Available DNS plugins: Cloudflare, Route 53, Google Cloud DNS, DigitalOcean, Linode, OVH, and many others. Install the plugin for your DNS provider:

# Snap-based Certbot (recommended)
sudo snap install certbot-dns-cloudflare

# Pip-based Certbot
sudo pip install certbot-dns-cloudflare

Monitoring Certificate Expiry

Do not rely solely on Certbot to renew certificates. Set up independent monitoring:

# Simple bash check for a cron job alert
EXPIRY=$(echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2)
EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))

if [ "$DAYS_LEFT" -lt 14 ]; then
  echo "WARNING: example.com cert expires in $DAYS_LEFT days" | \
    mail -s "Certificate Expiry Warning" ops@example.com
fi

Better options for production monitoring:

  • Prometheus + blackbox_exporter: Probes HTTPS endpoints and exports probe_ssl_earliest_cert_expiry metric. Alert when <14 days.
  • UptimeRobot / Pingdom: Built-in SSL monitoring with email alerts on approaching expiry.
  • SecureBin SSL Checker: On-demand certificate inspection showing chain, expiry, and configuration issues.

Decode Your SSL Certificate

Use SecureBin's Certificate Decoder to inspect the full details of your Let's Encrypt certificate including the chain, SANs, and key usage.

Decode Certificate

Certbot vs Alternatives

If Certbot is consistently problematic in your environment, consider alternatives:

  • acme.sh: Pure bash ACME client. No dependencies. Supports 50+ DNS providers. Installs as a cron job by default. Often more reliable in minimal environments (containers, embedded systems).
  • Caddy: Web server with automatic HTTPS built in. No separate certificate management needed. Caddy handles issuance, renewal, and OCSP stapling internally.
  • Traefik: Reverse proxy with built-in ACME support. Handles certificate management for dynamically discovered services (Docker, Kubernetes).
  • cert-manager (Kubernetes): Kubernetes-native certificate management. Issues and renews Let's Encrypt certificates as Kubernetes resources.

The Bottom Line

Let's Encrypt renewal failures come down to five root causes: port 80 inaccessibility, DNS challenge misconfiguration, rate limit exhaustion, stale ACME account credentials, and missing web server reload hooks. Run certbot renew --dry-run to identify which one you are hitting. Fix the specific issue, verify with another dry run, then confirm the live certificate with openssl s_client. Set up monitoring that is independent of Certbot itself, because a silent renewal failure gives you at most 90 days before users see certificate errors, and if renewal has been failing for weeks, you may have only days.

Related Articles

Continue reading: Fix Docker OOM Killed, Fix Kubernetes CrashLoopBackOff, Fix AWS EFS Permission Denied, API Key Rotation Best Practices, How to Secure API Keys in Code.

UK
Written by Usman Khan
DevOps Engineer | MSc Cybersecurity | CEH | AWS Solutions Architect

Usman has 10+ years of experience securing enterprise infrastructure, managing high-traffic servers, and building zero-knowledge security tools. Read more about the author.