Let’s Encrypt has become the default SSL certificate provider for the majority of developer-operated websites. The pitch is simple: free 90-day certificates renewed automatically by certbot. The reality is that “automatically” means “a systemd timer or cron job runs certbot on your server, and if it succeeds, you get a new certificate.”
There are a surprising number of ways that can fail without producing any visible error:
Any of these situations leaves you with a certificate that looks fine until it doesn’t — with no warning. This guide covers how to monitor Let’s Encrypt renewal so you know about problems days or weeks before they affect users.
Start by knowing where you stand. Run this on your server to see the current expiry:
sudo certbot certificates
This shows all managed certificates and their expiry dates:
Found the following certs:
Certificate Name: yourdomain.com
Domains: yourdomain.com www.yourdomain.com
Expiry Date: 2026-11-15 10:30:00+00:00 (VALID: 41 days)
Certificate Path: /etc/letsencrypt/live/yourdomain.com/fullchain.pem
Private Key Path: /etc/letsencrypt/live/yourdomain.com/privkey.pem
You can also check remotely from anywhere using openssl or the free SSL Certificate Checker:
echo | openssl s_client -connect yourdomain.com:443 -servername yourdomain.com 2>/dev/null \
| openssl x509 -noout -enddate
Certbot has a --email flag that was required during initial setup. If you included it, certbot already sends renewal failure notifications to that address — but only if certbot itself detects the failure. It does not send alerts if the systemd timer is simply not running.
Check which email is registered:
sudo certbot show_account
If the account is associated with an email you still check, renewal failures will be emailed. The ACME server (Let’s Encrypt) also sends expiry reminders at 20 days and 7 days before expiry — but only if the certificate renewal has not happened yet at that point, and only to the registered email.
The most direct way to check if renewal would work is a dry run:
sudo certbot renew --dry-run
This runs the full renewal process — validating the ACME challenge, checking the web root, testing HTTP-01 or DNS-01 challenges — without actually replacing the certificate. If it exits with a non-zero status, renewal is broken.
Run this as a cron job to get an early warning before the real renewal attempt fails:
# Add to root's crontab: crontab -e
# Run dry-run check weekly on Sunday at 6am; email on failure
0 6 * * 0 /usr/bin/certbot renew --dry-run --quiet || echo "certbot dry-run failed on $(hostname) at $(date)" | mail -s "ALERT: Let's Encrypt renewal dry-run failed" you@example.com
For this to work, the mail command must be configured. On Ubuntu/Debian, install mailutils and configure it with a sendmail-compatible relay.
An alternative that doesn’t require local mail configuration is monitoring the live certificate’s expiry date and posting to an external alert service. This is more reliable because it tests the actual certificate users see, not the renewal mechanism:
#!/bin/bash
# /usr/local/bin/ssl-expiry-check.sh
DOMAIN="yourdomain.com"
WARN_DAYS=21
SLACK_WEBHOOK="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
EXPIRY_DATE=$(echo | openssl s_client -connect "$DOMAIN":443 \
-servername "$DOMAIN" 2>/dev/null \
| openssl x509 -noout -enddate 2>/dev/null \
| cut -d= -f2)
if [ -z "$EXPIRY_DATE" ]; then
MSG="SSL CHECK FAILED: Could not retrieve certificate for $DOMAIN"
curl -s -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"$MSG\"}" "$SLACK_WEBHOOK"
exit 1
fi
EXPIRY_EPOCH=$(date -d "$EXPIRY_DATE" +%s 2>/dev/null \
|| date -jf "%b %d %T %Y %Z" "$EXPIRY_DATE" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))
if [ "$DAYS_LEFT" -le "$WARN_DAYS" ]; then
MSG="WARNING: SSL certificate for $DOMAIN expires in ${DAYS_LEFT} days ($EXPIRY_DATE)"
curl -s -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"$MSG\"}" "$SLACK_WEBHOOK"
fi
Add it to crontab to run daily:
# Run at 7am daily
0 7 * * * /usr/local/bin/ssl-expiry-check.sh
Replace the Slack webhook with an email API call (Mailgun, Resend, SendGrid) if you prefer email over Slack:
# Send via curl to Mailgun
curl -s --user "api:YOUR_MAILGUN_KEY" \
https://api.mailgun.net/v3/YOUR_DOMAIN/messages \
-F from="monitor@yourdomain.com" \
-F to="you@example.com" \
-F subject="SSL expiry warning: $DOMAIN" \
-F text="$MSG"
Modern Debian/Ubuntu systems run certbot via a systemd timer rather than cron. Check its last run:
# View recent certbot runs and their outcomes
sudo systemctl status certbot.timer
sudo journalctl -u certbot.service --since "7 days ago"
If the last renewal attempt failed, it appears in the journal with exit status and error details. This is the most direct diagnostic tool for systemd-based setups.
To get notified on failure, add OnFailure= to the certbot service unit:
# /etc/systemd/system/certbot-notify.service
[Unit]
Description=Notify on certbot failure
[Service]
Type=oneshot
ExecStart=/usr/local/bin/ssl-expiry-check.sh
# Override certbot.service to add failure notification
sudo systemctl edit certbot.service
# Add:
[Unit]
OnFailure=certbot-notify.service
All of the above methods run on the same server that hosts the certificate. If that server goes down, has a misconfigured network, or its cron daemon stops, you get no alert. External monitoring checks your certificate from the outside — exactly as your users do — on a regular schedule.
What external monitoring catches that local monitoring misses:
Use the free SSL Certificate Checker to see your domain’s expiry date, issuer, and whether it will renew in time — no signup required.
Check Certificate → Get Automated AlertsHTTP-01 challenges require port 80 to be reachable from the internet. Check:
sudo ufw status | grep 80
# or
sudo iptables -L INPUT | grep -E "80|http"
If port 80 is blocked, switch to DNS-01 validation or open the port temporarily during renewal.
Certbot stores the challenge file in the webroot. If nginx or Apache is serving from a different directory than certbot expects:
cat /etc/letsencrypt/renewal/yourdomain.com.conf | grep webroot
Compare against your actual web server document root configuration.
# Check certbot logs for rate limit errors
sudo grep "too many" /var/log/letsencrypt/letsencrypt.log | tail -20
Let’s Encrypt allows 5 failed validation attempts per domain per hour. After hitting the limit, wait an hour before retrying.
sudo systemctl list-timers | grep certbot
# Should show upcoming run within 12 hours if timer is active
If the timer is inactive or missing, re-enable it:
sudo systemctl enable certbot.timer && sudo systemctl start certbot.timer
| Scenario | Certbot handles? | Needs external monitoring? |
|---|---|---|
| Successful renewal 30+ days before expiry | Yes | No |
| Renewal fails, you get email | Partially (registered email only) | Recommended |
| Timer/cron disabled | No | Yes |
| Server migration, wrong webroot | No | Yes |
| CDN/load balancer certificate | No | Yes |
| Monitoring multiple domains | Only on that server | Yes |
The most resilient setup combines local checks with external monitoring:
systemctl status certbot.timer.The first two steps are covered by certbot itself. The third and fourth are the gap that most teams skip until an expiry causes an incident.
SSLWatch handles external certificate monitoring across all your domains. Add a domain, choose your alert threshold (30, 14, 7, 1 days), and get an email before any certificate expires — whether it’s managed by Let’s Encrypt, AWS ACM, or a manually installed cert.
Join the SSLWatch Waitlist → Check a Certificate Free