Node.js verifies the server certificate chain during the TLS handshake. certificate has expired (code CERT_HAS_EXPIRED) means at least one certificate in that chain has a notAfter date in the past, according to the clock of the machine running Node. It might be the site's own certificate, an intermediate, or a root.
Error: certificate has expired
at TLSSocket.onConnectSecure (node:_tls_wrap:1600:34) {
code: 'CERT_HAS_EXPIRED'
}
Compare the related Node message unable to verify the leaf signature or UNABLE_TO_GET_ISSUER_CERT_LOCALLY, which signal a missing chain rather than an expired date. And the browser version is NET::ERR_CERT_DATE_INVALID.
Decision order
- Is the certificate on the server really expired? Check from outside.
- If the leaf is fine, is an intermediate or cross-signed root expired?
- If the server is fine for everyone else, is your machine's clock wrong, or is your trust store old?
- Only if it is an internal service, consider a private CA.
Step 1: read the dates from outside
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
Look at notAfter. Run the SSL Checker too, which shows the expiry and the full chain. If the leaf is expired, renew it. Common reasons on the server side:
- A Let's Encrypt renewal job silently failing. Run
sudo certbot renew --dry-runand read the error. See Let's Encrypt renewal failures. - A certificate renewed on disk but the web server never reloaded. Reload nginx or Apache:
sudo nginx -t && sudo systemctl reload nginx. - A renewed certificate installed on one server of a pool, not on the others. Test each backend IP:
openssl s_client -connect 203.0.113.10:443 -servername example.com. - A CDN or load balancer still serving an old uploaded certificate.
The step-by-step is in How to renew an SSL certificate and automate it.
Step 2: the chain, not the leaf
Sometimes the site certificate is valid but the server sends an expired intermediate, or an old cross-signed chain. The SSL Chain Debugger lists each certificate and its dates. Print the whole chain yourself:
echo | openssl s_client -connect example.com:443 -servername example.com -showcerts 2>/dev/null \
| awk '/BEGIN CERT/,/END CERT/' > chain.pem
openssl crl2pkcs7 -nocrl -certfile chain.pem | openssl pkcs7 -print_certs -noout
If the server sends an expired intermediate, fix the server's certificate bundle: serve the leaf followed by the current intermediates (fullchain.pem in Let's Encrypt, not cert.pem).
A different, well-known case: when an old root CA expires, clients with an outdated trust store but without a newer root can fail on servers whose chain still leads to the expired root. The long-term fix is updating the CA bundle on the client, or having the server present a chain that ends in a root the client trusts.
Step 3: the client machine
The error is relative to the clock on the machine making the request. A wrong date on a VM, CI runner, container host or embedded device makes valid certificates look expired (or not yet valid, CERT_NOT_YET_VALID).
date -u
timedatectl status # Linux: check "System clock synchronized: yes"
Fix NTP synchronization (sudo timedatectl set-ntp true). Containers share the host's clock, so fix the host. A long-suspended laptop or VM snapshot is a typical culprit.
Also consider an outdated CA bundle in an old base image: see CA certificates missing in a container. Node ships its own Mozilla root list, so the age of your Node version matters; upgrade to a supported release.
Step 4: internal and self-signed services
For services that use a private CA, do not disable verification. Point Node at the CA file:
NODE_EXTRA_CA_CERTS=/etc/ssl/private-ca.pem node app.js
Node reads this variable only at process startup; changing process.env later has no effect. If your private CA or internal certificate itself has expired, reissue it. Do not use NODE_TLS_REJECT_UNAUTHORIZED=0 in production: it turns off verification for the whole process and leaves you open to interception.
Prevent the next one
Certificates expire silently, then break everything at once. Monitor them:
- Set up SSL expiry alerts with an uptime monitor, which can warn before the date.
- Scan many hostnames at once with the Bulk SSL Checker.
- Alert on failed renewals, not only on expiry, so you have weeks of warning.
- Track the registration side with domain expiry watch.
Next steps
- Inspect the chain with the SSL Chain Debugger.
- Read SSL/TLS certificate errors explained.
- See the curl equivalent, curl 60: SSL certificate problem.
- Check expiry dates with the SSL Checker.
FAQ
The site works in my browser. Why does Node fail?
Browsers cache and fetch missing intermediates, and use the operating system's trust store. Node uses its own bundled roots and does not fetch intermediates, so it is stricter about incomplete or outdated chains.
Does the clock really matter?
Yes. Validity is checked against local time, so a clock that is a year ahead or behind produces expired or not-yet-valid errors.
Can I ignore certificate errors for one request?
Node lets you pass rejectUnauthorized: false, but it removes the protection TLS gives you. Use it only for local debugging.
How do I see which certificate in the chain expired?
Use openssl s_client -showcerts or the SSL Chain Debugger to read the dates of each certificate.
Get notified when this fix updates
Plus occasional practical tips on uptime and monitoring, nothing else. Unsubscribe anytime.
This page is general information written by our editorial team and updated when documentation changes. Servers and providers differ, so test changes safely first. See our editorial policy and disclaimer.