SSL certificates expire silently. There's no runtime error, no log entry, no notification from your hosting provider. One morning your site returns a browser security warning, your API clients start throwing UNABLE_TO_VERIFY_LEAF_SIGNATURE, and users start filing support tickets. Node.js gives you everything you need to catch this before it happens — if you wire it up yourself.
The tls module is part of Node.js core. No npm install required. Here's the minimal pattern for fetching a certificate's expiry date:
const tls = require('tls');
function checkSSLExpiry(hostname) {
return new Promise((resolve, reject) => {
const socket = tls.connect(
443,
hostname,
{ servername: hostname, rejectUnauthorized: false },
() => {
const cert = socket.getPeerCertificate();
socket.destroy();
if (!cert || !cert.valid_to) {
return reject(new Error(`No certificate returned for ${hostname}`));
}
const expiresAt = new Date(cert.valid_to);
const daysRemaining = Math.floor(
(expiresAt.getTime() - Date.now()) / (1000 * 60 * 60 * 24)
);
resolve({
hostname,
expiresAt: expiresAt.toISOString(),
daysRemaining,
issuer: cert.issuer?.O ?? 'unknown',
subject: cert.subject?.CN ?? hostname,
});
}
);
socket.setTimeout(5000, () => {
socket.destroy();
reject(new Error(`Timeout connecting to ${hostname}`));
});
socket.on('error', reject);
});
}
// Example usage
checkSSLExpiry('github.com').then(console.log).catch(console.error);
Sample output:
{
hostname: 'github.com',
expiresAt: '2026-11-29T23:59:59.000Z',
daysRemaining: 55,
issuer: 'Sectigo Limited',
subject: 'github.com'
}
false lets you inspect expiring certificates even when the chain is already broken. For production HTTPS requests you still want true. This flag only applies to the tls.connect call used to read the certificate — it doesn't affect how your application handles TLS elsewhere.
Real applications expose dozens of domains — apex domains, subdomains, API endpoints, CDN origins. Check them all in one pass:
const domains = [
'example.com',
'api.example.com',
'staging.example.com',
'www.example.com',
];
async function checkAllDomains(hostnames) {
const results = await Promise.allSettled(
hostnames.map(h => checkSSLExpiry(h))
);
return results.map((r, i) => ({
hostname: hostnames[i],
status: r.status,
...(r.status === 'fulfilled' ? r.value : { error: r.reason.message }),
}));
}
checkAllDomains(domains).then(results => {
results.forEach(r => {
if (r.status === 'rejected') {
console.error(`❌ ${r.hostname}: ${r.error}`);
} else if (r.daysRemaining <= 14) {
console.warn(`⚠️ ${r.hostname}: expires in ${r.daysRemaining} days`);
} else {
console.log(`✅ ${r.hostname}: ${r.daysRemaining} days remaining`);
}
});
});
Promise.allSettled (rather than Promise.all) ensures one unreachable domain doesn't abort checks for all the others.
Checking is only half the job. You also need to be notified. Add Nodemailer to fire an alert when any domain is within 30 days of expiry:
const nodemailer = require('nodemailer');
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: 465,
secure: true,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
async function sendExpiryAlert(result) {
await transporter.sendMail({
from: '"SSL Monitor" <alerts@example.com>',
to: process.env.ALERT_EMAIL,
subject: `⚠️ SSL expiry warning: ${result.hostname} expires in ${result.daysRemaining} days`,
text: [
`Domain: ${result.hostname}`,
`Expires: ${result.expiresAt}`,
`Days remaining: ${result.daysRemaining}`,
`Issuer: ${result.issuer}`,
'',
'Renew your certificate before it expires to avoid downtime.',
].join('\n'),
});
}
async function monitorAndAlert(hostnames, thresholdDays = 30) {
const results = await checkAllDomains(hostnames);
for (const r of results) {
if (r.status === 'fulfilled' && r.daysRemaining <= thresholdDays) {
console.warn(`Sending alert for ${r.hostname} (${r.daysRemaining} days remaining)`);
await sendExpiryAlert(r);
}
}
}
Wire it to a daily schedule so expiry alerts fire automatically:
const cron = require('node-cron');
// Run every day at 8am
cron.schedule('0 8 * * *', async () => {
console.log(`[${new Date().toISOString()}] Running SSL expiry check`);
await monitorAndAlert([
'example.com',
'api.example.com',
], 30);
});
Install the single dependency:
npm install node-cron
Nodemailer is optional — swap in any transactional email library (SendGrid, Resend, Postmark) that your stack already uses.
This setup works. But as soon as you operate it for more than a few domains or a few weeks, friction compounds:
Before building your own monitor, check your most important domains with the free SSL checker below. It calls a real TLS handshake and returns days remaining, issuer, and expiry date — no signup required.
Enter any domain to instantly see days remaining, expiry date, and issuer. No signup required.
Check a domain now →If you want automated alerts at 30, 14, 7, and 1 day before expiry — without running your own Node.js process — SSLWatch monitors your domains and emails you before they expire.
Add your domains once. Get email alerts at 30, 14, 7, and 1 day before each certificate expires. No infrastructure to maintain.
Get early access → Free checker →