← Blog  ·  October 2026

SSL Certificate Monitoring with Python: Check Expiry and Get Alerts

How to programmatically check SSL certificate expiry across multiple domains and automate alerts using only Python's standard library.

SSL certificate expiry is one of the most avoidable causes of production outages. The certificate is usually set to expire a year or more out, and then somebody forgets to renew it. Browser warnings, API call failures, and angry users follow. The fix is straightforward: automated monitoring.

Python's standard library includes everything you need to check SSL certificate expiry without installing any third-party packages. Here's how to do it, from a single domain check to a full monitoring script with email alerts.

What this guide covers:

1 Check a single SSL certificate with Python

Python's ssl and socket modules let you connect to any HTTPS host and inspect its certificate. No third-party libraries required:

import ssl
import socket
from datetime import datetime

def get_ssl_expiry(hostname: str, port: int = 443) -> dict:
    """Get SSL certificate expiry info for a hostname."""
    context = ssl.create_default_context()
    
    try:
        with socket.create_connection((hostname, port), timeout=10) as sock:
            with context.wrap_socket(sock, server_hostname=hostname) as ssock:
                cert = ssock.getpeercert()
    except ssl.SSLCertVerificationError as e:
        return {"hostname": hostname, "error": str(e), "valid": False}
    except (socket.timeout, ConnectionRefusedError) as e:
        return {"hostname": hostname, "error": str(e), "valid": False}
    
    # cert['notAfter'] looks like: 'Dec 25 23:59:59 2025 GMT'
    expiry_str = cert['notAfter']
    expiry_date = datetime.strptime(expiry_str, '%b %d %H:%M:%S %Y %Z')
    days_remaining = (expiry_date - datetime.utcnow()).days
    
    return {
        "hostname": hostname,
        "expires_at": expiry_date.isoformat(),
        "days_remaining": days_remaining,
        "issuer": dict(x[0] for x in cert['issuer']).get('organizationName', 'Unknown'),
        "valid": days_remaining > 0,
    }

# Example usage
result = get_ssl_expiry("example.com")
print(f"{result['hostname']}: {result['days_remaining']} days remaining")
# → example.com: 82 days remaining

This works for any publicly reachable HTTPS host. It respects the certificate chain, verifies against system CA roots, and raises a proper error if the certificate is already invalid.


2 Check multiple domains

For most real-world use cases, you need to check more than one domain. Here's a script that checks a list and prints a summary:

import ssl
import socket
from datetime import datetime

DOMAINS = [
    "example.com",
    "api.yourapp.com",
    "staging.yourapp.com",
    "mail.yourapp.com",
]

WARN_THRESHOLD_DAYS = 30
CRITICAL_THRESHOLD_DAYS = 7

def get_ssl_expiry(hostname: str, port: int = 443) -> dict:
    context = ssl.create_default_context()
    try:
        with socket.create_connection((hostname, port), timeout=10) as sock:
            with context.wrap_socket(sock, server_hostname=hostname) as ssock:
                cert = ssock.getpeercert()
        expiry_date = datetime.strptime(cert['notAfter'], '%b %d %H:%M:%S %Y %Z')
        days_remaining = (expiry_date - datetime.utcnow()).days
        return {"hostname": hostname, "days_remaining": days_remaining, 
                "expires_at": expiry_date.isoformat(), "error": None}
    except Exception as e:
        return {"hostname": hostname, "days_remaining": -1, 
                "expires_at": None, "error": str(e)}

def main():
    results = [get_ssl_expiry(domain) for domain in DOMAINS]
    
    for r in results:
        if r["error"]:
            print(f"❌ {r['hostname']}: ERROR — {r['error']}")
        elif r["days_remaining"] <= CRITICAL_THRESHOLD_DAYS:
            print(f"🔴 {r['hostname']}: CRITICAL — {r['days_remaining']} days")
        elif r["days_remaining"] <= WARN_THRESHOLD_DAYS:
            print(f"🟡 {r['hostname']}: WARNING — {r['days_remaining']} days")
        else:
            print(f"✅ {r['hostname']}: OK — {r['days_remaining']} days")

if __name__ == "__main__":
    main()

Sample output:

✅ example.com: OK — 82 days
🟡 api.yourapp.com: WARNING — 18 days
🔴 staging.yourapp.com: CRITICAL — 3 days
❌ mail.yourapp.com: ERROR — [Errno 111] Connection refused

3 Add email alerts with smtplib

For automated monitoring, you need the script to actually alert you. Python's smtplib can send email using Gmail, SendGrid, or any SMTP provider:

import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

def send_alert_email(results: list, smtp_host: str, smtp_port: int,
                     username: str, password: str, 
                     to_email: str, from_email: str):
    """Send an alert email for domains with expiry warnings."""
    warnings = [r for r in results if r.get("days_remaining", 999) <= 30 or r.get("error")]
    
    if not warnings:
        return  # Nothing to alert on
    
    subject = f"SSL Certificate Alert: {len(warnings)} domain(s) need attention"
    
    body_lines = ["The following SSL certificates need attention:\n"]
    for r in warnings:
        if r["error"]:
            body_lines.append(f"❌ {r['hostname']}: Connection error — {r['error']}")
        else:
            body_lines.append(f"⚠️  {r['hostname']}: Expires in {r['days_remaining']} days ({r['expires_at'][:10]})")
    
    body = "\n".join(body_lines)
    
    msg = MIMEMultipart()
    msg["From"] = from_email
    msg["To"] = to_email
    msg["Subject"] = subject
    msg.attach(MIMEText(body, "plain"))
    
    with smtplib.SMTP_SSL(smtp_host, smtp_port) as server:
        server.login(username, password)
        server.sendmail(from_email, to_email, msg.as_string())
    
    print(f"Alert sent to {to_email} for {len(warnings)} domain(s)")

4 Run as a cron job

Once your script works interactively, schedule it to run daily. On Linux/macOS:

# Run SSL check every day at 9am
0 9 * * * /usr/bin/python3 /home/user/ssl_monitor.py >> /var/log/ssl_monitor.log 2>&1

On Heroku Scheduler:

python ssl_monitor.py

Or as a GitHub Actions scheduled workflow:

name: SSL Certificate Check
on:
  schedule:
    - cron: '0 9 * * *'  # Daily at 9am UTC
  workflow_dispatch:

jobs:
  check-ssl:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run SSL check
        run: python ssl_monitor.py
        env:
          SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
Important: If you're running this on a cron job, make sure to handle the case where the script itself fails — a Python exception or network timeout means you get no output and no alert. Consider adding a dead man's switch ping at the end of the script so you know it ran.

5 Handling edge cases

Wildcard certificates

Checking *.example.com directly will fail. Instead, check a specific subdomain that uses the wildcard cert (e.g., www.example.com or api.example.com). The expiry date on the wildcard cert is the same regardless of which subdomain you check.

Self-signed certificates

By default, Python's SSL context rejects self-signed certificates. To check one without verifying the chain:

context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE  # Only for self-signed/internal certs

This returns the certificate data without raising a verification error. Use with caution — CERT_NONE disables all validation.

Non-443 ports

For HTTPS services on non-standard ports (e.g., 8443), pass the port explicitly:

get_ssl_expiry("internal-api.company.com", port=8443)

Connection timeouts

The 10-second timeout in the examples covers most cases, but if you're checking many domains from a slow network, consider using concurrent.futures.ThreadPoolExecutor for parallel checks:

from concurrent.futures import ThreadPoolExecutor, as_completed

with ThreadPoolExecutor(max_workers=10) as executor:
    futures = {executor.submit(get_ssl_expiry, domain): domain for domain in DOMAINS}
    results = [future.result() for future in as_completed(futures)]

Limitations of a DIY approach

A custom Python script handles basic monitoring, but there are gaps when you're managing more than a handful of domains:

For a few domains, the DIY approach is fine. For production infrastructure with many domains, a dedicated monitoring service handles the failure modes, retries, and alerting that a script can't.

SSLWatch — Automated SSL Certificate Monitoring

SSLWatch monitors your SSL certificates and sends alerts at 30, 14, 7, and 1 day before expiry. No script to maintain, no cron job to monitor. Add your domains and get notified before anything expires.

Join the waitlist → Check a cert now (free)

Related reading