An expired SSL certificate is not a soft failure. It takes the whole site down at once.
Most write-ups on SSL treat an expired certificate like a warning you can dismiss, something that nags a few cautious visitors while everyone else carries on. That is not how browsers handle it. When the certificate date is invalid, Chrome, Safari, and Firefox stop rendering your site entirely and replace it with a full-screen interstitial: NET::ERR_CERT_DATE_INVALID, "Your connection is not private." Almost nobody clicks through that. The page your visitor wanted is simply gone, and so is every form, login, and API call behind it.
The cruel part is that the failure is binary and global. There is no slow degradation, no canary, no partial rollout. One minute the padlock is green for everyone, the next minute every new visitor hits a wall. And the reason it sneaks up on teams is that the thing meant to prevent it, auto-renewal, fails silently for boring, undramatic reasons. This post covers what actually breaks, why "it works in my browser" is not proof, and the four certificate properties worth checking on a schedule.
What's in this post
- Why an expired cert is total, not partial
- Why "it works in my browser" is a lie
- Auto-renewal fails silently, and why
- The trust chain: self-signed and missing intermediates
- Protocol version and the apex-vs-www gotcha
- Checking all of it in one scan
Why an expired cert is total, not partial
A 404 takes down one URL. A bad deploy might take down one route. An expired TLS certificate takes down the origin. The browser validates the certificate during the TLS handshake, before it has fetched a single byte of HTML, so the failure happens upstream of everything your application does. It does not matter that your server is healthy, your database is up, and your HTML is perfect. The handshake fails, and the browser refuses to proceed.
That is also why it is not just "the homepage looks broken." Anything that speaks HTTPS to your origin breaks at the same instant:
WHAT BREAKS WHEN THE CERT EXPIRES
---------------------------------------------------------------
Page loads | full-screen interstitial, no content rendered
Form submits | POST never reaches the server
Fetch / XHR | requests reject with a network error
Third-party API | webhooks and server-to-server calls fail TLS
Mobile apps | anything pinned to your HTTPS endpoint dies
Crawlers | Googlebot cannot complete the handshake
The interstitial is intentionally hard to bypass. Browsers want it to be hard, because the whole point of TLS is that an invalid certificate could mean an active attacker. From the browser's perspective an expired cert and a hostile one are indistinguishable, so it errs on the side of stopping the user cold. The web.dev explainer on why HTTPS matters is blunt about this: there is no minor breakage, and there is no safe way for users to opt out at scale.
Why "it works in my browser" is a lie
Here is the trap that lets a broken certificate sit live for hours. You get the alert, you open the site, the padlock is green, and you close the ticket. The site is still down for everyone else.
Two layers of caching are lying to you. First, browsers and the OS keep validated certificates and TLS session state around. Once your browser has completed a successful handshake with your origin, it can resume that session without doing a full re-validation, so you keep loading the site happily while a visitor with a cold connection gets rejected at the handshake. Second, you almost certainly have an HSTS and connection state warmed up that a first-time visitor does not.
The fix is to test the way a stranger's browser does: a fresh connection, no cached session, validating the chain from scratch. Do not eyeball the padlock. Read the live certificate over the wire.
# Read the live cert and print its validity window.
# This does a fresh handshake, like a brand-new visitor.
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -noout -dates -subject -issuer
# notBefore=...
# notAfter=Sep 3 12:00:00 2026 GMT <- this is the line that matters
The notAfter date is the one that takes you down. If you want a single number to alert on, compute days remaining and treat anything less than or equal to 14 days as urgent and anything already past as a live outage, not a warning:
# Days until expiry for a host. Negative means already expired.
expiry=$(echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
echo $(( ( $(date -jf "%b %e %T %Y %Z" "$expiry" +%s) - $(date +%s) ) / 86400 )) days left
The point is not the exact one-liner, which varies by platform (the date -jf syntax above is BSD/macOS; GNU/Linux uses date -d). The point is that the check has to originate from outside your warmed-up browser session, because your browser is the one client on earth most likely to tell you everything is fine.
Auto-renewal fails silently, and why
Most certificates today are short-lived and renewed by a robot, which is good, until the robot stops. Let's Encrypt issues certificates that are valid for 90 days and the standard advice is to renew with about 30 days of life left, so a healthy setup replaces the cert roughly every 60 days with a 30-day safety buffer. That buffer is the only thing standing between a single failed renewal and an outage, and it expires quietly.
Renewal fails for unglamorous reasons, and none of them throw an error your monitoring would see:
WHY AUTO-RENEWAL QUIETLY STOPS
---------------------------------------------------------------
DNS changed | the ACME challenge can no longer be validated
Cron stopped | the renewal timer was removed or never enabled
Rate limited | too many issuance attempts hit the CA's cap
Port 80 blocked | the HTTP-01 challenge can't reach the server
Billing lapsed | a paid CA's card expired, issuance suspended
Reload skipped | cert renewed on disk but the server never reloaded
That last one is a classic: certbot renews the file, but nothing reloads nginx, so the process keeps serving the old certificate from memory until it expires. The renewal "succeeded" and the site still goes down. The failure mode is always the same shape: something that used to run every couple of months stopped running, and nobody noticed because nothing was on fire. The only reliable defense is an external check on the actual served certificate, on a schedule, that does not trust the renewal job's own logs.
The trust chain: self-signed and missing intermediates
Expiry is the most common way a certificate breaks, but "not trusted" is the second, and it produces the same full-screen interstitial. A certificate is trusted only when it chains up to a certificate authority that browsers ship in their root store. Two failures account for most "not trusted" verdicts.
The first is a self-signed certificate, where you signed it yourself instead of a recognized CA. It is perfectly valid cryptographically and completely untrusted by every browser, because nothing vouches for it. This is common when a staging cert leaks into production or someone generates a quick cert to "make HTTPS work."
The second is sneakier and far more common in production: a missing intermediate certificate. Your server is supposed to send the leaf certificate plus the intermediate(s) that link it to the trusted root. If you install only the leaf, the chain is broken. The certificate is real, issued by a real CA, and not expired, but the browser cannot build a path from your certificate to a root it trusts, so it rejects it. The trap here is that this often "works in my browser," because some clients cache intermediates from past visits while a fresh client does not, the exact same illusion as before.
# Verify the chain the server actually sends. A clean chain ends in:
# Verify return code: 0 (ok)
# A missing intermediate shows:
# verify error:num=20:unable to get local issuer certificate
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| grep -E "Verify return code|verify error"
If you see unable to get local issuer certificate, you are missing an intermediate. Fix it by serving the full chain (the fullchain file your CA gives you), not just the leaf.
Protocol version and the apex-vs-www gotcha
Two more properties decide whether real clients can connect. The first is the TLS protocol version. TLS 1.0 and 1.1 are long deprecated and increasingly refused outright; modern clients expect TLS 1.2 at minimum, with TLS 1.3 preferred. A server stuck on an old protocol will see handshakes fail from up-to-date browsers even when the certificate itself is fine.
The second is the one that bites launches: Subject Alternative Names, the list of hostnames a certificate actually covers. A certificate issued for example.com does not automatically cover www.example.com. They are different names, and the SAN list has to include both. The failure is asymmetric and easy to miss: you test the apex, it loads, you ship, and every visitor who types or links the www host hits a name-mismatch interstitial.
# List the names the certificate actually covers.
# If you serve www but only example.com is listed, www is broken.
echo | openssl s_client -servername www.example.com -connect www.example.com:443 2>/dev/null \
| openssl x509 -noout -ext subjectAltName
# X509v3 Subject Alternative Name:
# DNS:example.com, DNS:www.example.com
The rule is simple: every hostname you actually serve over HTTPS, including the redirect source, needs to be in the SAN list of the certificate that host presents. A redirect from www to apex still requires www to complete its own handshake first, so the www host needs a valid, covering certificate even if it only ever redirects. While you are checking certificates, it is worth confirming your security headers too, because HSTS in particular interacts directly with how strictly browsers enforce all of this.
Checking all of it in one scan
Every check above can be done by hand with openssl, and you should know how, but doing it on a schedule, from outside your own warmed-up browser, across both your apex and www hosts, is exactly the kind of boring recurring task that quietly stops happening, the same way the renewal cron did. That is how certificates expire on live sites run by people who absolutely knew better.
The SEO stakes are real but secondary. HTTPS has been a lightweight Google ranking signal since 2014, and Chrome marks plain HTTP as "Not Secure," but the bigger risk is not the missing ranking nudge. It is that a crawler hitting a broken certificate cannot complete the handshake at all, so it cannot reach your HTTPS pages, and over repeated failed crawls that costs you far more than the modest HTTPS boost was ever worth.
The LintPage SSL Certificate Checker does the fresh-connection check for you: it connects to your domain over TLS, reads the live certificate, and reports the expiry date and days remaining, the issuer and subject, whether the chain is trusted (catching self-signed certs and missing intermediates), the TLS protocol version negotiated, and the Subject Alternative Names so you can confirm both apex and www are covered. It also confirms that HTTPS is served and that HTTP redirects to it.
A broken certificate rarely travels alone with the other things that quietly break a launch, so it is worth running the full pre-launch SEO checklist and the complete audit at the same time.
The 30-second version
An expired or untrusted TLS certificate is not a soft warning, it is a full-screen interstitial (NET::ERR_CERT_DATE_INVALID) that almost no visitor clicks past, so it takes the whole origin down at once: pages, forms, APIs, and crawlers all fail at the handshake. "It works in my browser" proves nothing, because your browser caches validated certs and resumes sessions while a fresh visitor gets rejected, so always read the live certificate over a cold connection. Auto-renewal (Let's Encrypt issues ~90-day certs and renews around the 30-day mark) fails silently for dull reasons: DNS changes, a dead cron, rate limits, a lapsed card, or a server that renewed the file but never reloaded. Watch four properties: days remaining (treat less than or equal to 14 as urgent), a trust chain that is not self-signed and not missing an intermediate, a modern TLS version, and a SAN list that actually covers both your apex and your www host. Check them from outside your warmed-up browser, on a schedule, because that schedule is the exact thing that quietly stops running.