Skip to main content

Monitoring

How to check an SSL certificate’s expiration date, and get warned in time

Checking one certificate takes one command. Knowing that every certificate you serve will still be valid next month is harder. This guide covers both: the commands, the failures they miss, and how to monitor certificates so a failed renewal reaches you before it reaches your users.

Check it now

The quickest way is the SSL checker: enter a hostname and see the expiry date, the issuer, the names the certificate covers and whether the chain is valid. To do it yourself, use one of these.

openssl

# Print the expiry date (notAfter)
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
  | openssl x509 -noout -enddate
# notAfter=Jan 14 23:59:59 2027 GMT

# Exit code 1 if the certificate expires within 14 days (1209600 seconds)
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
  | openssl x509 -noout -checkend 1209600

Keep -servername. Without it, a server that hosts several sites can send a different certificate than the one your visitors get.

curl

curl -sv https://example.com -o /dev/null 2>&1 | grep -i "expire date"
# *  expire date: Jan 14 23:59:59 2027 GMT

Python

import socket, ssl
from datetime import datetime, timezone

def days_left(host: str, port: int = 443) -> int:
    ctx = ssl.create_default_context()  # verifies the chain and the hostname
    with socket.create_connection((host, port), timeout=10) as sock:
        with ctx.wrap_socket(sock, server_hostname=host) as tls:
            not_after = tls.getpeercert()["notAfter"]
    expires = datetime.fromtimestamp(ssl.cert_time_to_seconds(not_after), timezone.utc)
    return (expires - datetime.now(timezone.utc)).days

print(days_left("example.com"))

Node.js

import tls from 'node:tls'

const socket = tls.connect({ host: 'example.com', port: 443, servername: 'example.com' }, () => {
  const { valid_to } = socket.getPeerCertificate()
  const days = Math.floor((new Date(valid_to) - Date.now()) / 86_400_000)
  console.log(valid_to, days, 'days left', socket.authorized ? '' : socket.authorizationError)
  socket.end()
})

Why certificates still expire on teams that automate renewal

Most expired certificates were set to renew automatically. The renewal broke, and nobody noticed. The usual causes:

  • The renewal job stopped. A server rebuild dropped the timer, a DNS API token used for validation was rotated, or the job hit the CA’s rate limit. It fails quietly in a log nobody reads.
  • The new certificate was never loaded. The file on disk is fresh, but the web server or load balancer still serves the old one from memory.
  • One endpoint was missed. The main site renewed; the API subdomain, the CDN origin or a second region still has the old certificate.
  • The chain is incomplete. Browsers fill in a missing intermediate certificate, so the site looks fine. curl, payment webhooks and mobile apps refuse the connection.
  • Nobody sends reminders any more. Let’s Encrypt ended its expiry emails in 2025, and maximum certificate lifetimes are getting shorter (200 days since March 2026, falling to 47 days by 2029). Renewals happen more often, so there are more chances for one to fail.

A one-off check catches none of this next month. You need the same check, on a schedule, against every hostname you serve, with an alert that reaches someone.

The do-it-yourself version

A cron job with openssl works if you keep the host list up to date and make sure its output reaches a person:

#!/usr/bin/env bash
# /etc/cron.daily/cert-check : mail a warning 21 days before any listed certificate expires
for host in example.com api.example.com shop.example.com; do
  if ! echo | openssl s_client -servername "$host" -connect "$host:443" 2>/dev/null \
       | openssl x509 -noout -checkend $((21*86400)) >/dev/null; then
    echo "$host: certificate expires within 21 days or could not be checked" \
      | mail -s "TLS warning: $host" [email protected]
  fi
done

The weak points: it runs on one server (if that server dies, the warnings stop), it depends on local mail delivery, and openssl x509 does not verify the chain, so an incomplete chain passes.

Monitoring certificates with Uptrack

An SSL monitor opens a new TLS connection on every check and verifies the full chain and the hostname, the same way a strict client does. The monitor goes down, and you get an alert, when:

  • the handshake fails (broken chain, wrong hostname, untrusted issuer),
  • the certificate has expired, or
  • it expires within the warning window: 14 days by default, set with cert_warning_days.

Add one monitor per hostname you serve. In the dashboard, choose the SSL type. With the REST API:

curl -X POST https://api.uptrack.app/api/monitors \
  -H "Authorization: Bearer $UPTRACK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "api.example.com certificate",
    "url": "https://api.example.com",
    "monitor_type": "ssl",
    "interval": 3600,
    "settings": { "cert_warning_days": 30 }
  }'

An hourly check is plenty for expiry. HTTPS uptime monitors also fail on certificate errors, so a broken chain on a page you already watch shows up within minutes. If you use Claude, the Uptrack MCP server can create these monitors for you: “add an SSL monitor for each of my API hostnames”.

Options compared

OptionCatches a failed renewalEvery endpointBroken chainWho hears about it
Calendar reminderNoNoNoOne person
CA expiry emailsSome CAsNoNoOne inbox
Cron job + opensslYesYes, if you list themPartlyYou build it
Uptrack SSL monitorYesYes, one monitor eachYesEmail, Slack, Discord, webhooks

Other monitoring services offer SSL checks too, some on free plans. Compare how often they check, whether they verify the chain, and where their alerts can go.

When you do not need this

  • Your only certificates are issued and served by a platform such as a managed host or CDN that renews them and owns the outcome.
  • Your certificates are on a private network. Uptrack checks run from public probes and do not connect to private addresses.

FAQ

How do I check an SSL certificate expiration date from the command line?

Run: echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -noout -enddate. The notAfter line is the expiry date. Add -checkend 1209600 instead of -enddate to get a non-zero exit code when fewer than 14 days are left.

How many days before expiry should I be warned?

At least 14 days, so a failed renewal has time to be fixed during working hours. Teams that renew by hand or through a ticket queue often use 30 days. Uptrack warns 14 days ahead by default; set cert_warning_days on the monitor to change it.

My site works in the browser. Why does a check report a certificate chain error?

Usually the server does not send its intermediate certificate. Desktop browsers can fetch the missing intermediate on their own, but curl, API clients, payment webhooks and many mobile apps cannot. The fix is to serve the full chain (for example fullchain.pem instead of cert.pem).

Do auto-renewing certificates like Let’s Encrypt still need monitoring?

Yes. Monitoring checks the certificate your servers actually serve, so it catches a renewal job that stopped, a renewed certificate that was never reloaded, and an endpoint that still serves the old one. Let’s Encrypt stopped sending expiry reminder emails in 2025, so nothing else warns you.

Can Uptrack monitor certificates on internal hosts?

No. Checks run from public probes and only connect to public addresses, so hosts on a private network or behind a VPN cannot be checked.

Hear about a failed renewal weeks before it expires

SSL monitors are included in every plan, from $3/mo, with a 30-day money-back guarantee.

See plans

More detail on the feature: SSL certificate monitoring.