SSL Certificates Expire Silently. Here's How I Monitor 50 Sites Without Paying for a Service.
SSL certificate expiry is the most embarrassing and preventable outage in web operations. I've watched it happen to large companies. Here's what to check, when to check it, and how to build a free monitoring setup.
At 2:14 AM on a Tuesday, I got paged for a revenue-critical checkout flow showing a "Your connection is not private" browser error. The cert on our CDN edge layer had expired six hours earlier — at 8 PM, right after the US east coast wound down. The origin cert was fine. Certbot was running. Let's Encrypt had renewed successfully. Nobody had thought to separately monitor the certificate on the Cloudflare custom domain. That outage cost roughly four hours of engineering time and a measurable drop in that night's conversion rate. The fix took 90 seconds once we knew what it was.
That night I built a small monitoring script. Since then I've extended it to cover 50 domains across personal projects, client sites, and internal tooling — all for free. This post documents exactly what I built, why SSL expiry keeps happening even when teams think they've automated it, and how ByteWaveNetwork's SSL Certificate Monitor lets you audit any domain in seconds without installing anything.
Key takeaways
- Let's Encrypt auto-renewal silently fails more often than teams expect — cron jobs break on server updates, DNS changes, and Docker restarts.
- CDN and load-balancer certificates are separate from origin certs. Monitoring one does not mean monitoring the other.
- 90-day certs expire three times faster than annual certs — the renewal window is only 30 days and most alerts fire too late for enterprise change management.
- A Node.js script + cron + Resend email can monitor 50 domains at zero cost.
- TLS 1.0 and 1.1 are EOL everywhere that matters. If you're still serving them, you have a compliance problem today.
Why SSL Expiry Keeps Happening Despite "Automation"
Every engineering team I've talked to says the same thing before an expiry incident: "We had that automated." Here are the four reasons automation fails in practice.
a) Certbot timers silently break
Let's Encrypt renewal relies on either a cron job or a systemd timer running certbot renew. When you update the OS, rebuild a container, or migrate to a new server, the timer doesn't follow. There's no email, no alert, no error — the job simply stops running. The cert renews fine right up until the day the old server was last running, then quietly stops.
b) Wildcard certificates don't auto-renew everywhere
Wildcard certs (*.domain.com) require DNS-01 challenge validation. Not every hosting provider or DNS provider supports the Certbot DNS plugin for your registrar. In those cases the renewal is either manual or silently skipped. I've seen a wildcard cert covering 200 subdomains expire because the DNS provider switched APIs and the plugin started returning 403s — with no alerting.
c) CDN/load-balancer certs are a separate surface
This is the scenario I described above. When you serve traffic through Cloudflare, AWS CloudFront, or a load balancer with SSL termination, there are at least two certificate surfaces: the origin cert and the edge cert. Certbot only renews the origin. The CDN cert is managed through its own console, its own renewal mechanism, and its own expiry clock. Teams monitor origin uptime. Nobody monitors the CDN cert until users start reporting browser errors.
d) SaaS custom domains give you no visibility
If you've added a custom domain to a SaaS product — Webflow, Ghost, Shopify, Notion Sites — the cert is issued and managed by the SaaS platform, not you. You can't check it via Certbot. You often can't even see the expiry date in the dashboard. The only reliable way to know is to connect to port 443 and read the certificate directly.
What the ByteWaveNetwork SSL Certificate Monitor Shows You
I built the SSL Certificate Monitor using the same TLS probe approach we use in the Page Speed Inspector — a Node.js tls.connect() call directly to port 443, no third-party libraries, no external APIs. You enter a domain name. The tool returns a complete certificate report within a couple of seconds.
Here's exactly what the output looks like for a typical domain:
example.comCertificate expiry: 2026-10-14 | Days remaining: 45 days (amber — renew soon)
Issuer / CA: Let's Encrypt (R11)
TLS version: TLS 1.3
Cipher suite: TLS_AES_128_GCM_SHA256
HSTS: Enabled (max-age=31536000; includeSubDomains)
SANs: example.com, www.example.com
Chain validity: Valid (3 certificates, root trusted)
The colour coding is immediate: green means more than 30 days remaining, amber means 8–30 days (renew now), red means fewer than 7 days or already expired. For most teams, the amber band is the action zone — it's visible enough to act on without triggering a crisis.
The Subject Alternative Names section is particularly useful if you have a wildcard or multi-domain cert — you can see exactly which hostnames are covered and immediately spot if a new subdomain you added is actually missing from the cert.
Comparing SSL Monitoring Options
| Tool | Cost | TLS version check | HSTS check | SAN visibility | Bulk / API | No install needed |
|---|---|---|---|---|---|---|
| ByteWaveNetwork SSL Monitor | Free | ✓ (1.2 / 1.3) | ✓ | ✓ | Script below | ✓ |
| SSL Labs (Qualys) | Free | ✓ (full grade) | ✓ | ✓ | Rate-limited API | ✓ |
| Uptime Robot (paid tier) | $7/mo+ | ✗ | ✗ | ✗ | ✓ | ✓ |
| StatusCake (pro) | $24/mo+ | ✗ | ✗ | ✗ | ✓ | ✓ |
| Screaming Frog SEO Spider | £199/yr | Partial | ✓ | ✗ | CLI mode | ✗ (desktop app) |
SSL Labs gives the most thorough TLS grading — if you need a full cipher suite audit for a compliance report, use Qualys. ByteWaveNetwork's tool is optimised for rapid daily monitoring and at-a-glance expiry status, not a one-time audit. Screaming Frog will crawl HSTS headers but doesn't surface cert expiry dates in a usable way. The paid uptime services alert you to expiry but don't tell you why — they don't show chain validity, TLS version, or SANs.
SSL Certificate Status Reference
| Status | What it means | What to do |
|---|---|---|
| Green (>30 days) | Certificate is valid and not approaching expiry. | No action needed. Keep monitoring weekly. |
| Amber (8–30 days) | Renewal window is open. Cert will expire within a month. | Trigger renewal immediately. Don't wait for auto-renew — verify it runs and succeeds. |
| Red (<7 days) | Critical. Browsers may already be showing warnings on some older clients. | Escalate to on-call. Renew immediately. Check CDN edge cert separately. |
| Expired | Certificate is past its NotAfter date. All modern browsers block the site. | Emergency renewal. Check whether auto-renewal is even configured. Post-mortem required. |
| Chain invalid | Intermediate certificate missing or misconfigured. Chain doesn't build to a trusted root. | Reinstall the full certificate chain. Check your server includes the intermediate bundle. |
| HSTS missing | HTTPS is working but the browser is not instructed to enforce it on future requests. | Add Strict-Transport-Security header with at least max-age=31536000. |
TLS Version Deprecation — Know Where You Stand
| TLS version | Status | Browser EOL | PCI DSS | Action |
|---|---|---|---|---|
| TLS 1.0 | Deprecated | Chrome 84 (Aug 2020), Firefox 78 | Non-compliant since Jun 2018 | Disable immediately. No exceptions. |
| TLS 1.1 | Deprecated | Chrome 84 (Aug 2020), Safari 14 | Non-compliant since Jun 2018 | Disable immediately. No exceptions. |
| TLS 1.2 | Accepted | Still supported | Compliant (with approved ciphers) | Acceptable short term. Target TLS 1.3 for new deployments. |
| TLS 1.3 | Recommended | N/A — current standard | Fully compliant | Default for all new and upgraded infrastructure. |
Certificate Authority Comparison
| CA | Cost | Cert lifetime | Validation types | Best for |
|---|---|---|---|---|
| Let's Encrypt | Free | 90 days | DV only | All web servers with automated renewal. Personal projects, startups, high-volume. |
| DigiCert | $200–$600+/yr | 1 year (max 398 days) | DV, OV, EV | Enterprise, financial services, EV green bar requirements, high-assurance applications. |
| Sectigo (formerly Comodo) | $70–$400/yr | 1 year | DV, OV, EV | Mid-market. Good wildcard pricing. Reseller ecosystem common in managed hosting. |
| AWS ACM | Free (with AWS resources) | 13 months (auto-renews) | DV | Any infrastructure running on AWS. Seamless ELB/CloudFront integration. |
For most teams running modern infrastructure, Let's Encrypt plus a solid monitoring setup is the right answer. Where I recommend DigiCert is specifically for OV/EV certificates where the organisation name in the cert itself matters to the business — typically regulated financial or legal services. The cost is the warranty and the assurance process, not the cryptography.
The Free 50-Domain Monitoring Script
Here's exactly what I run. It uses Node.js built-in tls and https modules only — no npm packages. Email goes via Resend's free tier (3,000 emails/month free, which is more than enough for daily cert checks on 50 domains).
// ssl-monitor.js
// Node.js 18+ — no external dependencies except 'resend' for email
// npm install resend
import tls from 'tls';
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
const DOMAINS = [
'bytewavenetwork.com',
'yourdomain.com',
// add up to 50 domains here
];
const ALERT_DAYS = 30; // amber threshold
const CRITICAL_DAYS = 7; // red threshold
function checkCert(domain) {
return new Promise((resolve, reject) => {
const socket = tls.connect(
{ host: domain, port: 443, servername: domain },
() => {
const cert = socket.getPeerCertificate(true);
const protocol = socket.getProtocol(); // 'TLSv1.3' or 'TLSv1.2'
const cipher = socket.getCipher();
socket.destroy();
if (!cert || !cert.valid_to) {
return reject(new Error(`No cert returned for ${domain}`));
}
const expiry = new Date(cert.valid_to);
const now = new Date();
const daysRemaining = Math.floor((expiry - now) / 86400000);
const sans = cert.subjectaltname
? cert.subjectaltname.replace(/DNS:/g, '').split(', ')
: [];
resolve({
domain,
expiry: expiry.toISOString().split('T')[0],
daysRemaining,
issuer: cert.issuer?.O || 'Unknown',
tlsVersion: protocol,
cipher: cipher?.name || 'Unknown',
sans,
status: daysRemaining < 0
? 'EXPIRED'
: daysRemaining <= CRITICAL_DAYS
? 'CRITICAL'
: daysRemaining <= ALERT_DAYS
? 'WARNING'
: 'OK',
});
}
);
socket.setTimeout(10000, () => {
socket.destroy();
reject(new Error(`Timeout: ${domain}`));
});
socket.on('error', reject);
});
}
async function run() {
const results = await Promise.allSettled(DOMAINS.map(checkCert));
const alerts = [];
for (const result of results) {
if (result.status === 'fulfilled') {
const r = result.value;
console.log(`[${r.status}] ${r.domain} — ${r.daysRemaining}d remaining (${r.tlsVersion})`);
if (r.status !== 'OK') alerts.push(r);
} else {
console.error(`[ERROR] ${result.reason.message}`);
}
}
if (alerts.length > 0) {
const html = alerts.map(r => `
<p><strong>${r.domain}</strong> —
${r.daysRemaining < 0 ? 'EXPIRED' : r.daysRemaining + ' days remaining'}<br>
Expiry: ${r.expiry} | CA: ${r.issuer} | TLS: ${r.tlsVersion}</p>
`).join('');
await resend.emails.send({
from: '[email protected]',
to: '[email protected]',
subject: `⚠️ SSL Alert: ${alerts.length} domain(s) need attention`,
html,
});
console.log(`Alert email sent for ${alerts.length} domain(s).`);
}
}
run();
Schedule it with a daily cron job: 0 8 * * * /usr/bin/node /opt/ssl-monitor/ssl-monitor.js >> /var/log/ssl-monitor.log 2>&1
SSL Certificate Monitoring Checklist
- Check the origin cert expiry date and days remaining.
- Check the CDN/edge cert expiry separately — this is a different cert from your origin.
- Verify TLS version is 1.2 or 1.3. Disable TLS 1.0 and 1.1 if still enabled.
- Check HSTS header is present with at least
max-age=31536000. - Verify the certificate chain is complete — intermediate certs must be included in your server config.
- Check SANs cover all hostnames you're serving — including both apex and www, and any new subdomains.
- Confirm auto-renewal is actually running — check systemd timer status (
systemctl status certbot.timer) or cron log output. - Check any SaaS custom domains (Webflow, Shopify, Ghost) — these are not covered by your server's Certbot.
- Set renewal threshold to 30 days remaining — not 14. Enterprise change management needs the runway.
- After any DNS change or server migration, re-verify all certs from scratch — assume nothing carried over.
- Run a quick check right now on your most critical domain at /tools/ssl-checker/.
Frequently Asked Questions
Why does Let's Encrypt use 90-day certificates instead of one year?
Let's Encrypt deliberately chose 90-day lifetimes to encourage automation and to limit the damage window if a private key is compromised. A stolen key from a one-year cert is exploitable for up to a year; a 90-day cert caps that exposure. The trade-off is that teams who aren't fully automating renewal will hit expiry three to four times per year instead of once. The 90-day design assumes your renewal is automated — if it isn't, you'll feel the shorter lifespan acutely.
What's the difference between a certificate expiring and a certificate being revoked?
Expiry is a scheduled, predictable event based on the NotAfter date embedded in the certificate at issuance. Revocation is an out-of-band action — the CA marks the cert as untrusted before its expiry date, usually because the private key was compromised or the domain ownership changed. Revocation is checked via OCSP or CRL. Most monitoring tools, including the script above, check expiry but not revocation status in real time. If you need revocation monitoring, use Qualys SSL Labs or a dedicated PKI management platform.
How far in advance should I renew my SSL certificate?
The technically correct answer is 30 days before expiry, which is also the window at which Let's Encrypt will accept a renewal request. The operationally correct answer for enterprise teams is earlier — at 45 days if you work in an environment with formal change management, ticket queues, or approval processes. Alerting at 14 days feels early until you factor in a two-week change freeze, a public holiday, or an on-call rotation gap. I alert at 30 days and treat anything under 14 days as a P1 incident.
Can I use this SSL checker to test certificates on non-standard ports?
The ByteWaveNetwork SSL Certificate Monitor probes port 443 by default, which covers the vast majority of public HTTPS traffic. The underlying Node.js tls.connect() approach in the monitoring script can be pointed at any port — useful if you're running HTTPS APIs on port 8443, SMTPS on 465, or LDAPS on 636. For public-facing sites, port 443 is always the right starting point, but the self-hosted script can be adapted by changing the port parameter in the tls.connect() call.
Check your SSL certificate right now — free, no sign-up required
Enter any domain and get back expiry date, days remaining, issuer, TLS version, HSTS status, SANs, and chain validity in seconds. Green, amber, and red colour coding makes the status immediately clear.
Open the SSL Certificate Monitor →No account needed. No rate limits for single-domain checks. Built on the same TLS probe engine as ByteWaveNetwork's Page Speed Inspector.
Transparency disclosure: ByteWaveNetwork may earn a commission from affiliate links in this post, including links to third-party services mentioned for comparison purposes. These do not affect editorial recommendations — tools are evaluated on technical merit. The Node.js monitoring script and all ByteWaveNetwork tools referenced are free to use without any affiliate relationship.