Guide · Certificates
How to check an SSL certificate's expiration date
Checked against sources on 2026-09-25 · 5 min read
Every TLS certificate carries two dates: notBefore, when it becomes valid, and notAfter, when it stops. Checking expiry means reading notAfter from the certificate a server actually sends. Below are the ways we use, from quickest to most scriptable, and the mistakes that make each one quietly give you the wrong answer.
In a browser
Open the site, click the icon at the left of the address bar, open the connection or certificate details, and look for the validity period ("Valid to", "Expires on" or similar). Every major browser has this, a couple of clicks deep, and the exact labels move around between versions.
It is fine for one site. It is not great for anything else: you see one hostname at a time, and browsers can paper over server mistakes. Firefox, for example, preloads intermediate CA certificates, so a server that forgets to send its intermediate can look fine in Firefox and still fail for curl, a mobile app or a payment provider's webhook.
If you want the answer without the clicks, our free SSL checker connects from our server and shows the expiry date and issuer for any hostname.
With openssl
This is the command we reach for first:
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -datesRun against letsencrypt.org on 25 September 2026, it printed:
notBefore=Sep 4 14:34:32 2026 GMT
notAfter=Dec 3 14:34:31 2026 GMTThe pieces: s_client opens a TLS connection. -servername sets SNI, the name the server uses to decide which certificate to send (RFC 6066 defines it). The echo closes the connection straight away so the command does not sit waiting for input. The second command reads the first certificate in the output, which is the server's own, and -dates prints both dates. The openssl x509 manual lists the rest of the options.
A few variations we use a lot:
-subject -issuershows who the certificate is for and who signed it.-ext subjectAltNamelists every hostname the certificate covers.-checkend 2592000exits non-zero if the certificate expires within the next 30 days (2,592,000 seconds). Handy in a cron job or a CI step.-showcertson thes_clientside prints every certificate the server sent, so you can see whether the intermediate is there.openssl x509 -in cert.pem -noout -enddatereads a certificate file on disk instead of a live server.
With curl
curl -vI https://example.com prints the TLS handshake details before the response headers. On builds that use OpenSSL, which is common on Linux, those details include start date, expire date, subject and issuer lines. We checked curl's OpenSSL code to be sure the lines are there.
The curl that ships with Windows uses Schannel, and in our test it printed none of those lines. The curl bundled with Git for Windows did the same. On Windows, use openssl from Git Bash, or PowerShell.
With PowerShell
No extra tools needed. This opens a TLS connection and reads the certificate with .NET:
$h = "example.com"
$tcp = [System.Net.Sockets.TcpClient]::new($h, 443)
$ssl = [System.Net.Security.SslStream]::new($tcp.GetStream())
$ssl.AuthenticateAsClient($h)
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($ssl.RemoteCertificate)
$cert | Select-Object Subject, Issuer, NotBefore, NotAfter
$ssl.Dispose(); $tcp.Dispose()NotAfter comes back in local time, as Microsoft's documentation says, so convert before comparing it with a UTC date from somewhere else. AuthenticateAsClient also validates the certificate. Against an expired certificate it throws an error ending in NotTimeValid instead of printing dates, which is itself a useful answer.
Checking many hosts
For a handful of hosts, a loop over a text file with one hostname per line is enough. The timeout stops one unreachable host from hanging the whole run:
while read -r host; do
end=$(echo | timeout 10 openssl s_client -connect "$host:443" -servername "$host" 2>/dev/null \
| openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
echo "$host ${end:-no certificate}"
done < hosts.txtPast a few dozen hosts you will want sorting by days left, retries, and a record of what changed since last week. Our bulk SSL checker checks a pasted list of hostnames in one go. To find hostnames you did not know about, the subdomain certificate finder searches certificate transparency logs, the public record of issued certificates.
Days left, not dates
A date like Dec 3 14:34:31 2026 GMT is easy to misread at a glance. For alerts and reports, days left is the number people actually act on. With GNU date (Linux, Git Bash) you can work it out in one line:
host=example.com
end=$(echo | openssl s_client -connect "$host:443" -servername "$host" 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
echo "$(( ( $(date -d "$end" +%s) - $(date +%s) ) / 86400 )) days left"The BSD date on macOS uses different flags, so on a Mac either install GNU coreutils or use -checkend with a number of seconds instead. -checkend is also the safer choice in scripts, because it answers yes or no through the exit code and never has to parse a date.
How often should you check? It depends on the lifetime. A 90-day certificate that renews with 30 days left gives a weekly check four chances to catch a failure. A 47-day certificate gives you about two weeks of slack, so check daily, and alert on "has not renewed yet" rather than waiting for a low days-left number.
What the fields mean
| Field | What it tells you |
|---|---|
| notBefore | The start of validity. A date in the future usually means a clock is wrong somewhere. |
| notAfter | The expiry. After this moment, clients reject the certificate. |
| Subject Alternative Name (SAN) | The hostnames the certificate covers. If the name you are checking is not in this list, the certificate is wrong for that host, whatever its dates say. |
| Issuer | Who signed it. Let's Encrypt only issues through ACME, so a Let's Encrypt certificate means some automation exists. Whether it still runs is another question. |
| Chain | The intermediate certificates the server sends along with its own. A missing intermediate breaks clients that cannot fill the gap themselves. |
Common gotchas
- Forgetting SNI. Without the right server name, a shared host may hand back its default certificate, which belongs to some other site. OpenSSL has sent SNI by default since 1.1.1, using the
-connecthost, but only when that host looks like a DNS name. Connect by IP address and you must pass-servernameyourself. - Checking the wrong host.
example.comandwww.example.comcan sit on different servers with different certificates. So canmail.,shop.and the staging box. Check every name that people or systems actually use. - Checking the edge, not the origin. Behind a CDN or load balancer, visitors see the edge certificate. If the CDN verifies your origin, an expired origin certificate breaks the site while the edge certificate looks fine. Check the origin with
-connect <origin-ip>:443 -servername example.com. - Missing intermediate. Run
s_clientwith-showcertsand count the certificates. Add-verify_return_errorand it stops with an error on a chain it cannot verify, instead of carrying on. - Time zones. openssl prints GMT. PowerShell prints local time. Mixing the two is how a certificate "expires" an afternoon early.
- Disk versus served. A certificate file on disk can be renewed while the web server keeps the old one in memory until it is reloaded. Always check what is served.
Or let something else remember
Checking by hand works right up to the week nobody does. ExpiryOwl checks every certificate every six hours and alerts at 30, 14, 7, 3, 1 and 0 days by email, Slack, Teams, Discord, Telegram or webhook. The free plan covers 5 domains with no card.
If you look after servers for several clients on your own, our page for freelance developers is written for you. And if you are wondering why these dates suddenly matter more, read about 47-day certificates.