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 CertificateError 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_expirymetric. 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 CertificateCertbot 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.
Usman has 10+ years of experience securing enterprise infrastructure, managing high-traffic servers, and building zero-knowledge security tools. Read more about the author.