← Blog  ·  October 2026

Monitoring Let’s Encrypt SSL Certificate Renewal: Know When Certbot Fails

Auto-renewal is only as reliable as the system running it. Here’s how to know when it breaks.

The silent failure problem: Let’s Encrypt certificates expire every 90 days. Certbot auto-renews them silently — but when the renewal fails (DNS misconfiguration, firewall change, port 80 blocked, disk full), there is no alert by default. Your first warning is a browser “Your connection is not private” page seen by users.

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.


Check Your Current Certificate Status

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

Method 1: Enable Certbot’s Built-In Email Notifications

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.

Limitation: Let’s Encrypt’s own emails are a last resort, not proactive monitoring. They tell you that renewal already failed and you’re now in the final countdown. For production services, you want to know 14–30 days before expiry, before your users see anything.

Method 2: Test Renewal Without Renewing (Dry Run)

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.


Method 3: Monitor the Certificate Directly via Cron

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"

Method 4: Check via Systemd Journal (for Systemd-Based Setups)

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

Method 5: External Monitoring (Most Reliable)

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:

Check Your Certificate From the Outside Right Now

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 Alerts

Troubleshooting Common Certbot Renewal Failures

Port 80 blocked

HTTP-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.

Web root mismatch

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.

Rate limit hit

# 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.

Timer not running

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

What to Monitor vs. What Certbot Already Handles

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

Recommended Setup for Production

The most resilient setup combines local checks with external monitoring:

  1. Keep the certbot systemd timer enabled and verify it runs weekly with systemctl status certbot.timer.
  2. Run a weekly dry-run via cron to catch configuration problems before they cause a real failure.
  3. Set up external certificate monitoring that checks the live certificate from outside your server on a daily schedule and alerts you at 30 and 14 days before expiry.
  4. Get alerted to the right channel — the same place you receive other production alerts (Slack, PagerDuty, email), not a mailbox you check monthly.

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.

Automate the Monitoring Part

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

Tags: ssl let's encrypt certbot monitoring  ·  October 2026