Short answer

A TLS handshake fails when client and server cannot agree on a common protocol version, a common cipher suite, or when the client cannot validate the server's certificate. In practice the failures fall into six buckets: protocol mismatch, cipher mismatch, missing or broken certificate chain, SNI problems, an expired/invalid certificate, and a required client certificate. Use openssl s_client to see exactly where it breaks.

First diagnosis: one command

Before guessing, see what the server actually returns. This shows the negotiated protocol, cipher and the full certificate chain:

openssl s_client -connect example.com:443 -servername example.com

The key lines: Protocol : (which TLS version was chosen), Cipher : (which cipher suite), Verify return code: (0 = OK, anything else is a validation error) and Certificate chain (the certificates the server sent). Omit -servername and you often hit the wrong certificate on multi-domain servers.

1. Protocol mismatch

The client only offers TLS 1.2+ while the server still runs TLS 1.0/1.1 — or the reverse. Modern clients now reject TLS 1.0 and 1.1. Test a specific version:

openssl s_client -connect example.com:443 -tls1_2
openssl s_client -connect example.com:443 -tls1_3

If -tls1_3 works but your application fails, it is probably running an old TLS library. The fix is to upgrade the client — not to re-enable legacy protocols on the server. See why old cryptography gets retired.

2. Cipher mismatch

Client and server share no common cipher suite. This typically appears after a hardening pass where the server only allows modern ciphers while an old client only speaks the legacy ones. List the server's accepted ciphers:

nmap --script ssl-enum-ciphers -p 443 example.com

If the list only contains TLS 1.3 ciphers and your client is older, that is the cause. A cipher suite describes key exchange, authentication, encryption and MAC all at once — all four parts must match.

3. Missing or broken certificate chain

The most common cause in production. The server sends its own certificate but forgets the intermediate certificate. Browsers often have the intermediate cached and show no problem, while curl, Java and mobile apps fail. The symptom is unable to get local issuer certificate. Count how many certificates the server sends:

openssl s_client -connect example.com:443 -servername example.com -showcerts

You should see at least two: your leaf certificate and one or more intermediates. If you only see one, the chain is incomplete. The fix is to install the full chain/fullchain certificate on the server.

4. SNI problems

On servers hosting several domains on one IP, TLS uses Server Name Indication to pick the right certificate. If the client sends no SNI, it gets the default certificate — which often does not match. That is why -servername is critical in the test above. Old clients and some API libraries do not send SNI by default.

5. Expired, not-yet-valid or self-signed certificate

An expired certificate gives Verify return code: 10. A self-signed certificate in the chain gives code 18 or 19. Check the validity dates directly:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -dates -subject -issuer

Server clocks that drift can also trigger a "not yet valid" error on a perfectly valid certificate — check NTP.

6. A required client certificate (mTLS)

If the server requires mutual TLS, the handshake fails when the client presents no valid client certificate. In the s_client output you will see Acceptable client certificate CA names. Supply a client certificate and key:

openssl s_client -connect api.example.com:443 -servername api.example.com \
  -cert client.crt -key client.key

Decision table: find your cause fast

Symptom Likely cause
unable to get local issuer certificateMissing intermediate (chain)
Works in browser, fails in curl/JavaMissing intermediate or trust store
no protocols availableProtocol mismatch
sslv3 alert handshake failureCipher mismatch or missing client cert
certificate has expiredExpired certificate

How to avoid handshake failures in production

Most handshake failures in production come from two things: expired certificates and missing intermediate chains after a renewal. Both are predictable. CertControl scans your endpoints from the outside, validates that the full chain is served correctly, and warns days before a certificate expires — so the handshake never fails for your users in the first place.

Frequently asked questions

What does "SSL handshake failed" mean?

That client and server could not establish an encrypted connection, because they could not agree on a protocol version or cipher, or because the server's certificate could not be validated. The error happens before any data is sent.

Why does the site work in my browser but fail in curl or Java?

Almost always a missing intermediate in the certificate chain. Browsers often cache intermediates from earlier visits, while curl and Java require the server itself to send the full chain. Install the fullchain certificate on the server.

How do I see which TLS version and cipher are used?

Run openssl s_client -connect host:443 -servername host and look at the Protocol : and Cipher : lines in the output.

Can a wrong server clock cause a handshake failure?

Yes. If the server clock drifts significantly, a valid certificate can appear "not yet valid" or expired. Synchronise the clock via NTP.

What is the difference between a handshake failure and a certificate warning?

A handshake failure aborts the connection entirely — no page loads. A certificate warning (e.g. in the browser) lets the user choose to continue. Both can stem from the same root cause, such as an expired certificate.