Let's Encrypt announced that they'll be Ending Support for Expiration Notification Emails on June 4th, 2025. These emails were sent to anyone who was issued an SSL certificate but didn't renew it in a timely manner, giving you a few weeks of grace period to fix the problem before the certificate actually expired and caused user-facing downtime! These emails continued a grand old tradition from the pre-ACME Certificate Authorities who'd email you multiple times in the weeks before expiration, reminding you to renew.
- The bad news: there's no more backstop from the CA to give you a friendly warning in advance—your users will surely let you know once it's fully expired!
- The good news: in many programming languages, it's just a few lines of code to check an SSL certificate's expiration date.
I'll show six code snippets below so you can ensure your team is alerted well in advance of any SSL certificates that are expiring soon.
But isn't SSL certificate renewal totally automated now?
Automated certificate renewals are great, but not entirely foolproof:
The projects above (and other ACME clients) are not to blame: if you browse through these issues, users are generally not finding bugs in the code. Instead, they're hitting a variety of configuration issues, DNS issues, auth tokens, timeouts, dependencies, etc. which break their automated certificate renewals.
As any crufty old engineer who's been doing this long enough knows, the automation will break eventually even if you do everything right. The Let's Encrypt emails were your last line of defense to prevent an avoidable outage, even if you never received one. Now it's up to you to check for upcoming expiration.
How many people could really be having this issue?
From Let's Encrypt's own announcement: "Providing expiration notifications costs Let’s Encrypt tens of thousands of dollars per year [...]" (emphasis added) Holey moley, that's a lot of expiration notification emails!
Let's Encrypt is providing a tremendously valuable service to the public, free of charge, and we are incredibly grateful. It's entirely reasonable that they wouldn't want to bear the operating costs of doing expiration notification.
This change just means that anyone responsible for an HTTPS website or API should probably take a few minutes and set up their own monitors to let them know if and when their certificate renewals are broken for whatever reason.
Code Snippets
Here are scripts in six different programming languages that all do the same thing:
- Connect to https://example.com:443/
- Read the server's SSL certificate "not after" date
- Calculate the number of
days
remaining - Have an
if
block that runs if there are less than 14 days until expiration, and anelse
block that runs if there are more.
I've personally tried to make these scripts as minimal as possible so they're easy for you to understand and customize. You should be able to copy and paste any of these and run them as-is:
Bash (shell script)
We use openssl s_client
to open a connection, and pipe its certificate into openssl x509 -noout -enddate
to extract a single field:
#!/bin/bash host="example.com" end_date=$(echo \ | openssl s_client -servername "$host" -connect "$host:443" 2>/dev/null \ | openssl x509 -noout -enddate \ | cut -d= -f2) # Convert a string like "Jan 15 23:59:59 2026 GMT" to seconds since epoch end_epoch=$(date -d "$end_date" +%s) now_epoch=$(date +%s) days=$(( (end_epoch - now_epoch) / 86400 )) if [ "$days" -lt 14 ]; then echo "WARNING! SSL certificate expires in only $days days! Somebody should fix it!" # TODO: Add alerting here else echo "ok: certificate valid for $days days" # TODO: Add a cronjob "OK" check-in here fi
Python
In Python, the tls.getpeercert()['notAfter']
contains the string we're looking for:
#!/usr/bin/env python3 import ssl, socket from datetime import datetime host = 'example.com' with socket.create_connection((host, 443)) as tcp: with ssl.create_default_context().wrap_socket(tcp, server_hostname=host) as tls: cert = tls.getpeercert() # Parse a string like "Jan 15 23:59:59 2026 GMT" into a datetime not_after = datetime.strptime(cert['notAfter'], '%b %d %H:%M:%S %Y %Z') days = (not_after - datetime.utcnow()).days if days < 14: print(f"WARNING! SSL certificate expires in only {days} days! Somebody should fix it!") # TODO: Add alerting here else: print(f"ok: certificate valid for {days} days") # TODO: Add a cronjob "OK" check-in here
Ruby
In Ruby, tls.peer_cert.not_after
is already a Time
instance:
#!/usr/bin/env ruby require "socket" require "openssl" host = "example.com" tcp = TCPSocket.new(host, 443) tls = OpenSSL::SSL::SSLSocket.new(tcp) tls.hostname = host tls.connect not_after = tls.peer_cert.not_after tls.sysclose tcp.close days = ((not_after - Time.now) / 86400).to_i if days < 14 puts "WARNING! SSL certificate expires in only #{days} days! Somebody should fix it!" # TODO: Add alerting here else puts "ok: certificate valid for #{days} days" # TODO: Add a cronjob "OK" check-in here end
Node.js (JavaScript)
In Node.js, socket.getPeerCertificate().valid_to
is a string
:
#!/usr/bin/env node const tls = require('tls'); const host = 'example.com'; const port = 443; const socket = tls.connect(port, host, {servername: host}, () => { const cert = socket.getPeerCertificate(); const notAfter = new Date(cert.valid_to); const days = Math.floor((notAfter - Date.now()) / (1000*86400)); socket.end(); if (days < 14) { console.log(`WARNING! SSL certificate expires in only ${days} days! Somebody should fix it!`); // TODO: Add alerting here } else { console.log(`ok: certificate valid for ${days} days`); // TODO: Add a cronjob "OK" check-in here } });
Go
In Go, conn.ConnectionState().PeerCertificates[0].NotAfter
is already a time.Time
:
package main import ( "crypto/tls" "fmt" "time" ) func main() { conn, err := tls.Dial("tcp", "example.com:443", nil) if err != nil { panic(err) } notAfter := conn.ConnectionState().PeerCertificates[0].NotAfter conn.Close() days := int(time.Until(notAfter).Hours() / 24) if days < 14 { fmt.Printf("WARNING! SSL certificate expires in only %d days! Somebody should fix it!\n", days) // TODO: Add alerting here } else { fmt.Printf("ok: certificate valid for %d days\n", days) // TODO: Add a cronjob "OK" check-in here } }
Powershell
In Powershell, the X509Certificate2
class has a NotAfter
property as a DateTime
:
$hostname = "example.com" $tcp = New-Object System.Net.Sockets.TcpClient($hostname, 443) $tls = New-Object System.Net.Security.SslStream($tcp.GetStream()) $tls.AuthenticateAsClient($hostname) # Upgrade to the newer X509Certificate2 class for its NotAfter property $cert2 = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($tls.RemoteCertificate) $notAfter = $cert2.NotAfter $tls.Close() $tcp.Close() $days = ($notAfter - (Get-Date)).Days if ($days -lt 14) { Write-Output "WARNING! SSL certificate expires in only $days days! Somebody should fix it!" # TODO: Add alerting here } else { Write-Output "ok: certificate valid for $days days" # TODO: Add a cronjob "OK" check-in here }
Using the scripts
- Replace
example.com
with your domain name. (Note: you may eventually want to monitor multiple domains, such asexample.com
andwww.example.com
andapi.example.com
.) - Make the
if
block do something that gets your attention. (Example: send an email, fire off a Heii On-Call alert, send a Slack message, etc.) - Make the
else
block do something that lets you know this monitoring script is still running regularly. (Example: check in with a Heii On-Call "Inbound Liveness" trigger.) - Deploy the script with a cron job to run daily.
That's it!
By embedding one of the above scripts into a daily cron job and plugging in your own notifications, you'll eliminate surprise outages from expired certificates. If you'd rather offload this entirely, Heii On-Call offers built-in SSL certificate expiration monitoring, alongside cron job heartbeats, HTTP uptime checks, on-call rotations, and mobile iOS and Android apps, so you're notified well before any preventable outages happen! And if you're a belt-and-suspenders type like me, you'd probably want to deploy one of the above the scripts internally, in addition to external monitoring.
For more information on SSL certificate monitoring, see also:
- SSL Certificate Expiration Monitoring which explains why we recommend setting up non-critical alerts at
14 days
until expiry, and critical alerts at2 days
until expiry. - Quickstart: SSL Certificate Monitoring in Heii On-Call for a few screenshots guiding you through the setup process.
You can set up triggers with "SSL Certificate Minimum Expiration Duration" on Heii On-Call's free plan in just a few minutes. Sleep easier knowing you'll be alerted well in advance of an easily avoidable customer-facing outage.