SSL certificate expiry is one of the most avoidable production outages in web infrastructure. Let's Encrypt made it dramatically better by automating renewal — yet certificates still expire and take down real services every day. The gap isn't the technology; it's the assumption that "auto-renewal is configured" equals "auto-renewal is working." SSL certificate expiry notifications exist to catch the difference.
Let's Encrypt's 90-day certificates with automated renewal via Certbot or similar clients work reliably — when everything is configured correctly and stays that way. In practice, several common patterns cause silent renewal failures:
On most systems, Certbot renewal runs via a systemd timer or a cron job. Either can fail without notice. A systemd timer can be masked, fail to start on a new OS installation, or be silently disabled by a package update. A cron job gets clobbered by a configuration management deploy or a server reimaging. Certbot's renewal mechanism is a scheduled job; scheduled jobs fail silently.
Check your Certbot timer right now:
systemctl status certbot.timer
# or
crontab -l | grep certbot
If either returns an error or nothing, your auto-renewal is not running.
Let's Encrypt validates domain ownership during renewal. The HTTP-01 challenge requires that /.well-known/acme-challenge/ on your domain returns a specific token. If a CDN, reverse proxy, or redirect rule blocks that path — and your infrastructure changes frequently — renewal can fail silently for months. Certbot logs the failure but doesn't send you an email by default.
Let's Encrypt enforces rate limits: 50 certificates per registered domain per week, and 5 duplicate certificates per week. A misconfigured deployment pipeline that requests a new certificate on every deploy can exhaust the rate limit, causing the next renewal attempt to fail with a 429 error. Again: Certbot logs this but doesn't email you.
Not every cert comes from Let's Encrypt. Commercial CAs (DigiCert, Sectigo, GlobalSign), internal PKI, client certificates, API authentication certificates, and certificates for services behind firewalls that can't complete ACME challenges are all outside Let's Encrypt's scope. These renew manually — or not at all, until someone remembers. A 1-year or 2-year commercial certificate doesn't trigger any alert when it expires unless you've set one up explicitly.
In a load-balanced setup, the certificate may be renewed on one node and not deployed to others. Or the certificate is stored in a central secret manager, renewed by a pipeline job, but the pipeline job silently fails for one environment. You end up with a renewed cert in staging and an expiring cert serving production traffic on two of your four nodes.
There are three main approaches to getting reliable expiry notifications.
Certbot supports pre/post/deploy hooks that run when a certificate is issued or renewed. You can use a post-expiry check hook to send an alert when a cert is approaching expiry. Add a deploy hook at /etc/letsencrypt/renewal-hooks/deploy/notify.sh:
#!/bin/bash
DOMAIN="$RENEWED_DOMAINS"
EXPIRY=$(certbot certificates 2>/dev/null \
| grep -A3 "$DOMAIN" \
| grep "Expiry Date" \
| awk '{print $3}')
echo "Certificate renewed for $DOMAIN. New expiry: $EXPIRY" \
| mail -s "SSL Renewed: $DOMAIN" ops@yourcompany.com
chmod +x /etc/letsencrypt/renewal-hooks/deploy/notify.sh
This tells you when a renewal succeeds, not when it fails. To catch failures, add a separate cron-based check:
#!/bin/bash
# Run daily — alerts when cert is within 21 days of expiry
DOMAIN="yourdomain.com"
EXPIRY=$(echo | openssl s_client -servername "$DOMAIN" \
-connect "$DOMAIN:443" 2>/dev/null \
| openssl x509 -noout -enddate \
| cut -d= -f2)
EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s 2>/dev/null \
|| date -j -f "%b %d %T %Y %Z" "$EXPIRY" +%s)
DAYS=$(( (EXPIRY_EPOCH - $(date +%s)) / 86400 ))
if [ "$DAYS" -lt 21 ]; then
echo "$DOMAIN expires in $DAYS days ($EXPIRY)" \
| mail -s "SSL EXPIRY WARNING: $DOMAIN" ops@yourcompany.com
fi
Limitations: requires a working MTA; only checks one domain; the script itself can fail silently.
For environments with multiple domains and no Let's Encrypt, a cron-based script that checks each domain against a threshold gives you a lightweight self-hosted notification system:
#!/bin/bash
THRESHOLD_DAYS=30
ALERT_EMAIL="ops@yourcompany.com"
DOMAINS=("api.yourapp.com" "app.yourapp.com" "admin.yourapp.com")
for DOMAIN in "${DOMAINS[@]}"; do
EXPIRY=$(echo | openssl s_client \
-servername "$DOMAIN" \
-connect "$DOMAIN:443" 2>/dev/null \
| openssl x509 -noout -enddate 2>/dev/null \
| cut -d= -f2)
if [ -z "$EXPIRY" ]; then
echo "Could not fetch cert for $DOMAIN" \
| mail -s "SSL CHECK FAILED: $DOMAIN" "$ALERT_EMAIL"
continue
fi
EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s 2>/dev/null)
DAYS=$(( (EXPIRY_EPOCH - $(date +%s)) / 86400 ))
if [ "$DAYS" -lt "$THRESHOLD_DAYS" ]; then
echo "$DOMAIN SSL cert expires in $DAYS days" \
| mail -s "SSL EXPIRY: $DOMAIN ($DAYS days)" "$ALERT_EMAIL"
fi
done
Add to crontab to run daily:
0 7 * * * /opt/scripts/ssl-check-all.sh >> /var/log/ssl-check.log 2>&1
The most reliable approach is an external service that checks your certificate from outside your infrastructure on a schedule and sends email alerts at multiple thresholds: 30 days, 14 days, 7 days, and 1 day before expiry. This works for any certificate — Let's Encrypt, commercial CAs, wildcard certs — and doesn't depend on your server's health, MTA configuration, or cron daemon.
SSLWatch takes this approach: enter a domain, get alerts. No agents, no scripts, no server-side changes required.
When you receive an SSL expiry notification, work through this checklist before panicking:
echo | openssl s_client -connect yourdomain.com:443 2>/dev/null | openssl x509 -noout -dates to confirm the live cert's expiry. Sometimes an alert fires for a cert that has already been renewed but the monitoring service hasn't picked up the new expiry yet.certbot certificates to see the expiry as Certbot sees it, and check /var/log/letsencrypt/letsencrypt.log for recent renewal attempts and any errors.certbot renew --dry-run to see if renewal would succeed without actually issuing a certificate./.well-known/acme-challenge/ is accessible on port 80 for HTTP-01 challenges.Before setting up notifications, verify what your current certificate looks like from the outside — the same view a browser sees:
See your certificate's expiry date, issuer, and chain validity in seconds. No signup, no install.
Check your SSL cert →SSLWatch monitors your SSL certificates externally and sends email alerts at 30, 14, 7, and 1 day before expiry. It works for any certificate on any server — Let's Encrypt, commercial, wildcard — without agents or server-side changes.
SSLWatch checks your certs from outside your infrastructure and emails you at 30, 14, 7, and 1 day before expiry — whether you're using Let's Encrypt, a commercial CA, or a wildcard cert.
Join the SSLWatch waitlist → Check your cert for free