DEV Community

Cover image for Domain Expiry and SSL Certificate Monitoring You Can Self-Host
Iurii Rogulia
Iurii Rogulia

Posted on Originally published at iurii.rogulia.fi

Domain Expiry and SSL Certificate Monitoring You Can Self-Host

A site going down because its TLS certificate expired is an embarrassing kind of outage: nothing actually broke. The code didn’t change, the server didn’t crash, traffic didn’t spike. A date on a certificate passed, and every browser hitting the site now shows a security warning instead of a homepage. Domain expiry is the same failure one layer up. The registration lapses and there’s no page at all, because the domain points nowhere or, worse, sits in a grace period where someone else can snap it up.

Both are avoidable months in advance. Both are, in practice, still one of the more common ways a working site goes dark, because the thing that’s supposed to prevent them – an email from the registrar or the certificate authority – has a habit of not doing its job.

Why the Renewal Email Isn’t Enough

The registrar-email safety net has three specific holes, not one vague ‘email might get missed’ problem:

  1. It only covers what it knows about. A registrar emails about domains registered through it. If a client migrated a domain to a different registrar two years ago and the old account still has a stale contact email on file, that renewal notice goes nowhere useful. A certificate issued for a subdomain by infrastructure you don’t directly administer – a CDN, a load balancer someone else configured – often has no renewal alert path back to you at all. Nothing was ever told to email you.
  2. The inbox it lands in isn’t reliably watched. Registrar and CA notices tend to go to whatever address was on file when the domain or the cert was first set up: a founder’s personal address after they’ve left, a shared alias nobody checks unless something’s already broken, sometimes a domain-specific address that’s itself expired.
  3. It’s a single notification, not a monitor. One email at 30 days out, filtered into a promotions tab or buried in the rest of the inbox, and nothing checks again. A monitor that polls on a schedule and keeps polling until someone acknowledges the problem is a different kind of safety net than an email that fires once and is done.

None of this is the registrar’s or the CA’s fault. They do exactly what they said they’d do. Their notification is one layer of defense, not the only one, and where a lapse costs a client relationship or a day of downtime, one layer isn’t enough.

What a Domain and SSL Certificate Check Actually Needs to Be

Domain expiry and certificate expiry are two different lookups with the same shape: a periodic poll, not a live health check. Neither needs to run more than once a day. Nothing about a registration or a certificate changes minute to minute, so a cron job is the right tool here, not an under-powered one.

Domain expiry is a WHOIS (or RDAP) lookup against the registry: read the expiration date, compare it to now, alert if the days remaining drop under a threshold. That’s the exact shape of check_domain() in Site Pulse, the open-source monitoring tool I built:

# website_monitor.py — sitepulse
def check_domain(domain: str) -> tuple[str, bool]:
    """Check domain registration expiry and nameservers"""
    w = whois.whois(domain)

    if isinstance(w.expiration_date, list):
        expiry_date = min(w.expiration_date)
    else:
        expiry_date = w.expiration_date

    days_until_expiry = (expiry_date - datetime.now()).days

    if days_until_expiry <= EXPIRY_WARNING_DAYS:
        messages.append(f"⚠️ Domain will expire in {days_until_expiry} days")
Enter fullscreen mode Exit fullscreen mode

Two details only show up when you run this against real domains instead of reading the WHOIS spec.

First, w.expiration_date isn’t reliably a single value. Some registrars return a list of dates, so the code has to check for that and take the earliest one, or it throws on exactly the domains where you most need it to work.

Second, the threshold is a config value, not a magic number baked into the comparison. Site Pulse defaults EXPIRY_WARNING_DAYS to 90, which leaves enough runway to actually action a renewal, rather than a 7-day warning that lands the same week as three other fires.

The same function in Site Pulse also checks the domain’s nameservers against an expected list, which catches a related failure: a DNS change nobody meant to make, silently pointing the domain somewhere it shouldn’t be.

Certificate expiry is the same pattern one layer down the stack: open a TLS connection to the host, read the certificate’s notAfter field, compare it to now, alert under the same kind of day threshold. I don’t have a shipped SSL-checking tool to show code from the way I do for the WHOIS side – Site Pulse checks domain registration and page health, not certificates – so I won’t paste a snippet and imply it’s been running in production, because it hasn’t. The shape of the check isn’t in question, though: same ‘connect, read one date field, compare, alert’ logic as the WHOIS check, talking TLS instead of WHOIS. It belongs in the same cron job, on the same schedule, feeding the same alert channel.

slug="automation-workflows"
text="A cron job that checks domain and certificate expiry and pings you before a renewal lapses is a small build – and it’s the kind of unattended automation I set up so nobody finds an outage from a customer complaint."
/>

Self-Host It, or Pay for the SaaS?

Neither ‘self-hosting is always cheaper’ nor ‘paid monitoring is always safer’ holds. Each side wins in a specific, identifiable situation.

Self-hosting wins when you already have somewhere to run a cron job. If a VPS or a scheduler is already running other maintenance tasks, adding a domain-and-cert check costs almost nothing: no per-domain subscription, no vendor account, no data about your infrastructure sitting on someone else’s dashboard. For a handful of domains you administer directly – your own sites, a few client domains where you already have WHOIS and DNS access – this is the right call almost every time. It’s the same reasoning behind self-hosting other small operational tools instead of paying SaaS pricing for what a cron job and a Telegram bot can do; see Self-Hosting a Node.js API on Caddy and Docker Compose for the same calculus applied to a different piece of infrastructure.

The paid SaaS wins when the cost of a missed alert is someone else’s business, not just your own. That’s the trade-off self-hosted monitoring doesn’t solve for free: who monitors the monitor. A self-hosted script can fail silently: an unhandled exception on a malformed WHOIS response, a cron job that stopped firing after a server reboot changed the crontab, a Telegram bot token that expired and nobody noticed, because the alert channel itself went quiet. A paid monitoring service has its own on-call rotation and SLA watching its own uptime, and that’s the layer a single self-hosted cron job doesn’t have behind it. If you’re running this for dozens of customer domains where a missed expiry costs a client relationship, not just an afternoon of your own downtime, that backstop is worth paying for. The subscription buys you someone else’s on-call engineer, not just the check itself.

The practical middle ground, if you do self-host: don’t let the monitor be a single point of failure itself. A dead man’s switch – a second, independent scheduled ping that alerts if the monitoring job hasn’t reported in within its expected window – closes most of the ‘who watches the watcher’ gap without the cost of a full SaaS subscription. It’s a small addition on top of the same cron infrastructure, and it turns ‘the script silently stopped running three weeks ago’ into its own alert instead of a domain quietly lapsing in the background.


A registrar email is a single point of failure with no second check behind it. A cron job that polls WHOIS and a TLS handshake on a schedule is cheap to build and, for a domain count you already administer, cheaper than a subscription – as long as something is also watching the job itself. If you’d rather this ran unattended than hear about an expired certificate from a customer, get in touch and I’ll set it up.


Further reading:

Top comments (1)

Collapse
 
furqan_ashraf profile image
Furqan Ashraf •

The min(w.expiration_date) fix in check_domain() is one of those details you only learn by running it against real domains. Another one coming up: since ICANN dropped the port-43 requirement for gTLDs in January 2025, some registrars' WHOIS servers return thin or empty text, and whois.whois() then hands back None for the expiry instead of throwing. An RDAP lookup first, with python-whois as the fallback, would cover most gTLDs with clean JSON dates. If you'd rather not maintain both paths, WhoisFreaks (whoisfreaks.com/documentation/whois-api#live-lookup) queries RDAP and port 43 and returns the same JSON either way (I work there). There's a one-time 500 free credits, and how complete the data is still depends on each registry. How many domains is Site Pulse watching for you? For a short list, the free RDAP route is probably all you need.