SSL certificate expiry is one of those problems that looks trivial until it bites you. Let’s Encrypt renewals are automatic — until something breaks in the automation and nobody notices for 30 days. Manually managed certificates expire when your calendar reminder gets ignored or the person who set the reminder left the company.
This guide covers every meaningful approach to SSL certificate expiry monitoring, from a free five-minute cron job to paid alerting services, so you can pick the right level of coverage for each environment.
Before setting up automated monitoring, it’s useful to know where you stand. You can check any domain’s certificate expiry in your browser’s address bar, or use the command line:
echo | openssl s_client -connect yourdomain.com:443 -servername yourdomain.com 2>/dev/null \
| openssl x509 -noout -dates
This returns the notBefore and notAfter dates directly. For a human-readable days-remaining count:
echo | openssl s_client -connect yourdomain.com:443 2>/dev/null \
| openssl x509 -noout -enddate \
| awk -F= '{print $2}' \
| xargs -I{} date -d "{}" +%s \
| xargs -I{} sh -c 'echo $(( ({} - $(date +%s)) / 86400 )) days remaining'
Or use the free SSL Certificate Checker tool to get a visual result instantly without touching the command line.
The most straightforward DIY approach is a cron job that checks certificate expiry and sends an email when the deadline gets close. Here’s a working shell script:
#!/bin/bash
# ssl-check.sh — checks a domain's cert and alerts if expiry < 30 days
DOMAIN="yourdomain.com"
ALERT_DAYS=30
ALERT_EMAIL="you@example.com"
EXPIRY=$(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" ]; then
echo "SSL check failed for $DOMAIN — could not retrieve certificate" \
| mail -s "SSL check error: $DOMAIN" "$ALERT_EMAIL"
exit 1
fi
EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s 2>/dev/null || date -j -f "%b %d %T %Y %Z" "$EXPIRY" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))
if [ "$DAYS_LEFT" -le "$ALERT_DAYS" ]; then
echo "SSL certificate for $DOMAIN expires in $DAYS_LEFT days ($EXPIRY)" \
| mail -s "⚠️ SSL expiry warning: $DOMAIN ($DAYS_LEFT days)" "$ALERT_EMAIL"
fi
Schedule it to run daily at 9am:
0 9 * * * /path/to/ssl-check.sh
This works reliably if your server has openssl and mail configured. The limitations show up quickly in practice:
For a single personal domain, this is fine. For production services or multiple domains, you need something more robust.
A slightly more robust approach replaces the mail dependency with a webhook POST to Slack or a generic HTTP endpoint:
#!/bin/bash
DOMAIN="yourdomain.com"
SLACK_WEBHOOK="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
ALERT_DAYS=30
EXPIRY=$(echo | openssl s_client -connect "$DOMAIN":443 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
DAYS_LEFT=$(( ( $(date -d "$EXPIRY" +%s) - $(date +%s) ) / 86400 ))
if [ "$DAYS_LEFT" -le "$ALERT_DAYS" ]; then
curl -s -X POST "$SLACK_WEBHOOK" \
-H "Content-Type: application/json" \
-d "{\"text\": \"⚠️ SSL expiry: $DOMAIN expires in $DAYS_LEFT days\"}"
fi
This removes the dependency on a local mail server, which is usually the point of failure in the first version. The cron job reliability problem remains: if the job silently stops running, your next alert about the cert is the browser warning your users see.
If you’re using Let’s Encrypt, the right answer is usually to make sure certbot renew is running correctly rather than monitoring expiry dates manually. A working auto-renewal setup makes the problem go away.
Check the status of your renewal timer:
# systemd (most modern Linux):
systemctl status certbot.timer
# or for snap-installed certbot:
systemctl status snap.certbot.renew.timer
# verify the renewal would succeed (dry run):
certbot renew --dry-run
The catch is that certbot renew can silently fail. Common failure modes:
/.well-known/acme-challenge/ pathRunning --dry-run manually every few months catches most of these, but it doesn’t give you proactive alerting when something breaks between manual checks.
certbot renew --dry-run in a cron job and alert on failure. This tests the renewal path without actually renewing, so it fails early rather than 30 days before expiry.
If your certificates are managed through AWS Certificate Manager (ACM), Google Cloud Certificate Manager, or Cloudflare, those platforms have built-in expiry notifications:
If you’re already on one of these platforms and using their certificate management, turn on their notifications. They’re free and require almost no setup.
Several services will check your certificate from outside your infrastructure and email you before it expires. The notable free options:
| Service | Free tier | Alert advance | Channels |
|---|---|---|---|
| UptimeRobot | 50 monitors, 5-min interval | 30 days | Email, Slack, webhook |
| StatusCake | 10 SSL monitors | 30 days | Email, webhook |
| Better Uptime | 10 monitors | Configurable | Email, SMS, Slack |
| Oh Dear | 14-day trial only | Configurable | Email, Slack, webhook |
| SSLWatch | Free tool, $9/mo for alerts | 60/30/7 days | Email alerts |
UptimeRobot’s free tier is the most commonly used option for indie developers: 50 monitors with 5-minute ping intervals and basic SSL expiry alerts. The SSL monitoring is a secondary feature rather than the primary focus, but it works reliably for most use cases.
For production environments where an expired certificate means real revenue impact, dedicated SSL monitoring tools offer more thorough coverage:
Get email alerts 60, 30, and 7 days before any certificate expires. Never manually track renewal dates again. Add any domain in seconds.
Join the waitlist Free checker toolThe right answer depends on your infrastructure:
certbot renew --dry-run is in a cron job that alerts on failure. This is the lowest-overhead setup for auto-renewed certs.There’s a common misconception that Let’s Encrypt auto-renewal makes SSL monitoring unnecessary. Let’s Encrypt renews certificates automatically — when the automation is working. The automation can break silently, and by the time you notice, you have 30 days left instead of 90.
The pattern that catches this: monitor the certificate expiry date externally, independent of whether you think auto-renewal is working. If the cert’s expiry date stops advancing (i.e., it’s no longer renewing), you’ll know 60 days before the expiry rather than on the day it fails.
This is why external monitoring is worth adding even if you have certbot configured: it validates that the automation is actually doing its job, not just that the job is scheduled to run.
One underused approach: add an SSL check to your deployment pipeline. After deploying, verify the certificate is valid and not within 30 days of expiry:
#!/bin/bash
# Add to your post-deploy CI step
DOMAIN="yourdomain.com"
DAYS_LEFT=$(echo | openssl s_client -connect "$DOMAIN":443 2>/dev/null \
| openssl x509 -noout -enddate \
| cut -d= -f2 \
| xargs -I{} date -d "{}" +%s \
| xargs -I{} sh -c 'echo $(( ({} - $(date +%s)) / 86400 ))')
if [ "$DAYS_LEFT" -lt 30 ]; then
echo "::warning::SSL certificate expires in $DAYS_LEFT days"
fi
This surfaces certificate issues in your deploy logs where developers are already looking, without requiring any external tooling. It complements rather than replaces dedicated monitoring — CI runs on deploy, not daily, so it won’t catch a cert that expires between deploys.
SSL certificate expiry is preventable with minimal investment. The key points:
certbot renew --dry-run in a cron job that alerts on failure — this catches renewal breakage early rather than 30 days before expiry.SSLWatch monitors your certificates and sends automated email alerts at 60, 30, and 7 days before expiry. Simple setup, $9/month.
Get early access Check your cert free