"SSL handshake failed" means the client and server started negotiating a secure connection and gave up partway: no agreed protocol version, no agreed cipher, a certificate one side refused, or a message one side never received. It is the umbrella diagnosis; the fix depends on which handshake step died. Cloudflare users see the same failure with a number on it: error 525.
What the Handshake Does#
In one round trip (TLS 1.3) the two sides agree on a protocol version and cipher suite, the server proves its identity with its certificate, and both derive the session keys. Each step is a distinct failure point:
| Step | Failure | Typical evidence |
|---|---|---|
| Version negotiation | No shared TLS version | protocol version alert in openssl |
| Cipher negotiation | No shared cipher suite | handshake failure alert immediately after ClientHello |
| Certificate validation | Expired, wrong name, broken chain, bad key usage | Verify return code non-zero |
| SNI routing | Server picks wrong vhost/cert for the name | Wrong certificate in openssl output |
| Transport interference | Proxy, firewall, or MTU eats handshake messages | Connection hangs or resets mid-handshake |
Client-Side Causes#
When one machine fails and others work: wrong system clock (certificate dates appear invalid), antivirus or corporate proxies intercepting TLS, an outdated OS or browser without modern protocol support, or a VPN mangling packets. The fix list is the same as for ERR_SSL_PROTOCOL_ERROR: clock, interception software, updates, then network.
Server-Side Causes#
When everyone fails:
- Legacy protocol config: only TLS 1.0/1.1 enabled; modern clients walk away. Fix per the cipher mismatch guide.
- Certificate problems: expired, name mismatch, or an incomplete chain; the handshake reaches verification and dies there.
- SNI misrouting: multi-tenant servers that present the default vhost's certificate when the requested name has no explicit config.
- Mutual TLS surprises: a server demanding client certificates fails every client that does not have one; common after enabling mTLS on the wrong listener.
- Proxy chain disagreements: with a CDN in front, the edge-to-origin leg has its own handshake; Cloudflare's 525 means specifically that leg failed, so the fix belongs on the origin.
What the Error Codes Actually Say#
"Handshake failed" is the umbrella message; the specific error underneath it depends on the client. Node.js and most other TLS stacks surface the underlying OpenSSL X.509 verify code directly:
| OpenSSL verify code | Meaning | Typical cause |
|---|---|---|
UNABLE_TO_VERIFY_LEAF_SIGNATURE | Unable to verify the first certificate | Broken or incomplete chain |
UNABLE_TO_GET_ISSUER_CERT_LOCALLY | Unable to get local issuer certificate | Client is missing the intermediate/root CA |
DEPTH_ZERO_SELF_SIGNED_CERT | Self-signed certificate | Server presents a self-signed cert to a client expecting a CA-signed one |
SELF_SIGNED_CERT_IN_CHAIN | Self-signed certificate in the chain | An untrusted root sits in the served chain |
CERT_HAS_EXPIRED | Certificate has expired | Renewal missed |
HOSTNAME_MISMATCH | Hostname mismatch | Certificate's SAN/CN doesn't match the requested domain |
curl reports the same underlying failures in its own wording (SSL certificate problem: unable to get local issuer certificate, certificate has expired), and browsers translate them into their own error pages rather than the raw OpenSSL string. Whatever the client, the verify code from openssl s_client (below) is the same code the client library is reacting to, so it is the fastest way to name the failure precisely instead of guessing from a vague client-side message.
Diagnose in Two Commands#
curl -v https://example.com 2>&1 | grep -E "SSL|TLS|certificate"openssl s_client -connect example.com:443 -servername example.com
curl -v https://example.com 2>&1 | grep -E "SSL|TLS|certificate"openssl s_client -connect example.com:443 -servername example.com
curl tells you whether and where the handshake fails; openssl shows the negotiated protocol, the certificate presented, the chain served, and the verify code. Between them you can name the failing step in under a minute, then apply the step-specific fix above.
Limitations to Know#
- "Handshake failed" is a symptom, not a diagnosis. Logs that record only this string force you to re-derive the failing step every incident; capture the alert detail or verify code where you can.
- Intermittent failures usually mean a pool. One bad backend behind a load balancer fails a fraction of handshakes while every manual test passes; test each backend directly.
Conclusion#
A failed handshake is one of five steps breaking: version, cipher, certificate, SNI, or the transport between the parties. Localize client-vs-server first, let openssl name the step, and the fix is the standard one for that step.
Handshake health is also invisible from inside a working session, which is why these failures run quietly for hours. ObserveOne performs a full fresh handshake from outside on every scheduled check, so the first one that fails becomes an alert instead of a trend in your bounce rate.