Writing

SSL handshake failed

Published 15 September 2026

Half a dozen unrelated systems emit these three words, and each one means something narrower than the others. Some write TLS handshake failed instead: SSL is the old name for TLS, and the failure is the same one. Which software wrote the line narrows the cause more than anything else you can do, so start there rather than with a generic checklist.

nginx: SSL_do_handshake() failed

In error.log, usually with an OpenSSL reason in brackets. The reason code is the whole message; the words around it are boilerplate.

Note that nginx logs these for inbound connections. If the failing direction is nginx talking to an upstream, look for proxy_ssl_* settings instead: a common miss is proxy_ssl_server_name on;, without which nginx sends no SNI to the backend.

Java: SSLHandshakeException

Java keeps its own trust store, entirely separate from the operating system's. A certificate the rest of the machine trusts perfectly well can still fail here, which is why "it works in curl" is not evidence of anything.

Run with -Djavax.net.debug=ssl:handshake. It is verbose and it is decisive: you see exactly which step failed rather than guessing.

Cloudflare error 525

A specific and frequently misread case. 525 means Cloudflare could not complete a handshake with your origin server: the visitor's connection to Cloudflare was fine. Nothing is wrong between the browser and the edge.

Almost always one of: no certificate installed on the origin at all, an expired origin certificate, the origin not accepting connections on 443, or SSL mode set to Full (Strict) with a self-signed certificate at the origin. Test the origin directly by IP, bypassing Cloudflare, and the real error appears immediately.

AWS load balancers

An ALB reports handshake failures in access logs and in the ClientTLSNegotiationErrorCount metric. The usual cause is a security policy that no longer permits what old clients offer: AWS retires policies periodically, and a policy chosen years ago may have been replaced underneath you.

Because the metric counts without explaining, it is worth enabling access logs before changing anything: the negotiated protocol and cipher appear per request, and the pattern of which clients fail tells you whether it is a protocol or a cipher problem.

curl, wget, Python and Go

These are client-side reports of a server-side refusal, and they share one set of causes. GnuTLS-based builds phrase it as peer failed to perform TLS handshake, which is worked through in detail there: six causes in likelihood order, including the two that are easy to miss: the server asking for a client certificate, and a path MTU black hole swallowing the server's Certificate message.

The command that works regardless of source

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

Whatever wrote the log line, this tells you what the server does when a known-good client asks. If OpenSSL succeeds and your application fails, the difference is in your application's TLS settings or its trust store, which is exactly the situation Java produces most often. If OpenSSL fails too, the problem is the server or the path.

That default test does not prove the connection used post-quantum key agreement. If the failure appeared after a TLS library upgrade or only affects a larger ClientHello, test the negotiated group explicitly; X25519MLKEM768 explains the hybrid exchange and the middlebox failure pattern it can expose.

A failure that appears only when SNI privacy is required belongs to a different branch. Test the DNS HTTPS record and require negotiation as described in the Encrypted Client Hello test; an ordinary successful request may have silently fallen back without ECH.

Check the server side without installing anything. The Kodura SSL checker reports which TLS versions a host accepts, the negotiated cipher, and the chain exactly as sent: with no silent repairs. That answers the protocol and cipher questions above in one step, from outside your network.

Open the SSL checker

The one thing worth not doing

Every source above has a documented way to accept weaker protocols, and it will make the error stop. It is almost never the right change: you are widening what a server accepts from everyone, to accommodate one client that should be updated instead. If the failing client genuinely cannot be updated (embedded hardware is the honest case) scope the exception to that path rather than the whole listener.