The short answer
SSL handshake failed means the two sides gave up before agreeing how to encrypt the connection, so no request was ever sent. The failure is not generic: the side that gave up almost always sends a numbered TLS alert, and that number names the fault — 40 handshake_failure, 48 unknown_ca, 70 protocol_version, 71 insufficient_security, 112 unrecognized_name. Read the alert or your client's error string before touching a setting. The fixes that skip that step — disabling verification, turning old protocols back on — remove the protection instead of the fault.
On this page
- What a handshake failure actually is
- Read the alert code first
- Diagnosing it in four commands
- What your client's error string means
- The intermediate only browsers forgive
- Version and cipher mismatches
- When the server doesn't recognise the name
- Cloudflare 525 and the origin leg
- Causes that are new in 2026
- The fixes that make it worse
- FAQ
What a handshake failure actually is
A TLS handshake is the negotiation that happens before any request is sent: the client proposes protocol versions, cipher suites and a hostname, the server picks from what it was offered, sends a certificate chain, and both sides prove they hold the keys they claim to. A handshake failure means that negotiation was abandoned. Nothing was encrypted badly, because nothing was encrypted at all.
That distinction is the reason so much debugging time goes nowhere. “SSL handshake failed” reads like a certificate error, so the certificate is the first thing anyone looks at — and in a large share of cases the connection died before the certificate was ever sent. A protocol version mismatch, a cipher suite mismatch and an unreachable port all surface under the same sentence.
What makes this tractable is that TLS is unusually talkative about its own failures. When one side gives up it is supposed to send an alert record carrying a numbered description, and those numbers are standardised in RFC 8446 for TLS 1.3 and RFC 5246 for TLS 1.2. Your client almost certainly printed one. It is usually buried in the middle of a long error string, which is why it gets skipped.
Read the alert code before you change anything
Every fatal TLS alert carries a number defined in the RFC 8446 alert registry, and that number narrows the cause to a single handshake stage. Alerts 40, 70 and 71 mean the two sides found no common version or cipher. Alerts 42 through 48 mean a certificate was received and rejected. Alerts 49 and 51 mean key or access checks failed, usually under mutual TLS.
One rule governs how to read any of them, and it is the rule most often got backwards: an alert that arrives at you was sent by the other side, about what it received from you. If your server logs an incoming unknown_ca, the client distrusts the chain your server sent. Changing your server's trust store will not help. The same alert logged by your client means the opposite, and the fix lives somewhere else entirely.
| Alert | What the sender means | Where to look |
|---|---|---|
| 40 handshake_failure | Could not negotiate an acceptable set of security parameters | Version or cipher overlap; a server with no certificate usable for what was asked |
| 42 bad_certificate | A certificate was corrupt, or its signature did not verify | A truncated or wrongly concatenated PEM file; in mutual TLS, the client certificate |
| 44 certificate_revoked | The certificate was revoked by its issuer | Reissue is the only fix; check why revocation happened before reordering |
| 45 certificate_expired | A certificate in the chain is outside its validity window | Check the intermediate too, not only the leaf; check the verifier's clock |
| 46 certificate_unknown | Some other problem made the certificate unacceptable | Often a name mismatch or a missing key usage; the catch-all of the certificate alerts |
| 48 unknown_ca | A chain was received but could not be matched to a trusted issuer | A missing intermediate, a private CA, or a stale trust store |
| 49 access_denied | The certificate was valid but the peer declined to proceed | Client-certificate access rules; a valid identity that is not authorised |
| 51 decrypt_error | A signature or key-exchange verification failed | In mutual TLS, a client key that does not match the client certificate |
| 70 protocol_version | The offered protocol version is recognised but not supported | A client stuck on TLS 1.0/1.1, or a server that has not enabled TLS 1.2+ |
| 71 insufficient_security | The server requires stronger parameters than the client offered | A hardened server policy meeting a legacy client; more specific than alert 40 |
| 112 unrecognized_name | The server did not recognise the name sent in SNI (RFC 6066) | A missing or wrong SNI value; a virtual host that is not configured |
Two alerts on that list are worth treating as good news. Alert 70 and alert 71 say the same thing as alert 40 but with more precision, so a peer that sends one of them has saved you a round of guessing. Alert 40 is the least specific of the three, and it is also by far the most common, which is why the next step is to reproduce the failure somewhere you can see both sides.
Diagnosing it in four commands
Four openssl s_client runs isolate almost every handshake failure. The first establishes whether TLS works at all from your network. The second pins a protocol version to test for a version mismatch. The third shows exactly which certificates the server sends. The fourth turns a quiet verification warning into a hard error you cannot overlook.
# 1. Does it negotiate at all, and with what? openssl s_client -connect example.com:443 -servername example.com </dev/null # 2. Pin a version to test for a version mismatch openssl s_client -connect example.com:443 -servername example.com -tls1_2 </dev/null openssl s_client -connect example.com:443 -servername example.com -tls1_3 </dev/null # 3. What does the server actually send? (chain order and completeness) openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null # 4. Make verification failures fatal instead of advisory openssl s_client -connect example.com:443 -servername example.com \ -verify_return_error </dev/null
Three lines in that output carry most of the answer. Protocol and Cipher tell you what was agreed, if anything. Verify return code tells you how the chain fared: 0 (ok) is a pass, 21 (unable to verify the first certificate) is the signature of a missing intermediate, and 10 (certificate has expired) needs no translation. The list of certificates under Certificate chain shows what the server sent, in order.
The most useful thing this gives you is a bisection. If openssl s_client completes a handshake from the same machine where your application fails, the server is negotiating correctly and the fault is in your application — its trust store, its pinned protocol versions, its cipher list, or a proxy it is configured to use. If s_client fails too, the fault is on the server side or in the network between you. That one comparison eliminates an entire half of the search space, and it takes about ten seconds.
When you want the same picture from outside your own network — which matters, because a corporate proxy that intercepts TLS will change the answer — you can check what chain your server serves to the public internet and compare it against what you saw locally.
What your client's error string is telling you
Every TLS stack words the same failures differently, which is why the same server misconfiguration is filed as four unrelated bugs by four teams. The strings below map back to two causes between them: a peer that sent a fatal alert, or a chain that could not be verified locally. Recognising which one you have decides whether you look at the server or at the client.
| Client | What you see | What it means |
|---|---|---|
| curl | (35) ... alert handshake failure | The peer sent alert 40 — version, cipher or certificate-selection mismatch |
| curl | (60) unable to get local issuer certificate | Verification failed locally — usually a missing intermediate on the server |
| Java | PKIX path building failed ... unable to find valid certification path | Same missing intermediate, or an issuer absent from the JDK trust store |
| Java | Received fatal alert: handshake_failure | The peer sent alert 40; on older JDKs, often an unsupported modern cipher |
| Python | CERTIFICATE_VERIFY_FAILED | Local verification failed; the trailing text names the specific reason |
| Go | x509: certificate signed by unknown authority | Chain could not be completed; Go performs no AIA fetching |
| Chrome | ERR_SSL_VERSION_OR_CIPHER_MISMATCH | No shared version or cipher — the browser wording for alert 40 / 70 / 71 |
| Cloudflare | Error 525 / Error 526 | The edge-to-origin handshake failed (525) or the origin certificate was rejected (526) |
The intermediate that only browsers forgive
If a site loads in Chrome but fails from curl, Java, Python or a mobile app, the server is almost certainly sending only its leaf certificate and omitting the intermediate that links it to a trusted root. Browsers paper over this. Desktop Chrome and Edge read the Authority Information Access extension and fetch the missing intermediate from the CA, and Firefox ships a preloaded set of intermediates through Mozilla's Remote Settings rather than fetching them.
Command-line tools and application runtimes do neither. curl, Java, Python and Go verify only the certificates the server actually presented, so an incomplete chain is a hard failure every time. The result is a server that has been quietly misconfigured for months and looks healthy to everyone who checks it the obvious way.
The fix is to install the full chain file your CA provides: the leaf first, then each intermediate in order. The root does not need to be sent and adds a wasted round of bytes to every connection, since a client that does not already hold the root has no reason to trust a copy the server hands it. How certificate chains are built and validated covers the ordering rules and the common ways bundles get assembled wrong.
One habit prevents the whole class of problem: after installing or renewing any certificate, verify it with something that is not a browser. A single curl -v https://yourhost/ catches an incomplete chain in the minute after deployment rather than in a support ticket six weeks later.
Version and cipher suite mismatches
A version or cipher mismatch is an empty intersection between two lists, and neither side is malfunctioning. The client offers the versions and cipher suites it is willing to use, the server compares them against its own list, and if nothing appears in both it sends alert 40, 70 or 71 and closes the connection. No certificate is involved, which is why reissuing one changes nothing.
The two directions this arrives from look identical in the logs and call for opposite fixes. An old client against a modern server — an embedded device, a legacy Java service, a payment terminal — fails because the server dropped TLS 1.0 and 1.1 and the RSA key-exchange ciphers. A modern client against an old server fails for the mirror-image reason. Running the second command in the previous section against each protocol version tells you which situation you are in within a minute.
Where the old component is the server, the work is a configuration update. Where the old component is a client you do not control, the honest answer is sometimes that it cannot connect to a properly configured endpoint, and the options are to update it or to give it a separate endpoint with a deliberately weaker policy, kept away from anything sensitive. Cipher suites explained sets out what a current suite list should contain and what has been removed from it.
When the server doesn't recognise the name
Server Name Indication is how a client tells a server which hostname it wants, before any certificate is chosen. On a host serving many sites from one address, a missing or wrong SNI value means the server picks its default virtual host, which usually holds a certificate for some other name. RFC 6066 says a server that does not recognise the name should either abort with a fatal unrecognized_name(112) alert or continue the handshake.
That choice is why the symptom is inconsistent. A server that aborts produces a clean alert 112 and an obvious diagnosis. A server that continues hands over the wrong certificate, and the client rejects it a moment later for a name mismatch — the same root cause, reported as a certificate problem. RFC 6066 also notes that sending unrecognized_name at warning level is not recommended, because client behaviour in response to warnings is unpredictable.
The clients that hit this are the ones that do not send SNI at all: very old runtimes, some monitoring probes, and anything connecting to a bare IP address rather than a hostname. You can reproduce it exactly by dropping -servername from the openssl commands above and watching a different certificate come back. What SNI does and why shared hosting depends on it covers the mechanism in full.
Cloudflare 525 and the origin leg
Behind a CDN there are two separate TLS connections: the visitor to the edge, and the edge to your origin server. Cloudflare error 525 refers only to the second. It means the handshake between the edge and your origin never completed, which can only happen in Full or Full (Strict) mode, where the edge is required to speak TLS to the origin. Error 526 is the neighbouring case: the origin did present a certificate and the edge rejected it.
The causes on that leg are the ordinary ones, just somewhere you cannot see from a browser: nothing listening on port 443 at the origin, a firewall that allows the edge on port 80 but not 443, an origin certificate that does not cover the hostname being requested, or no shared protocol version between the edge and an old origin stack. Each is diagnosable with the same openssl commands, pointed at the origin address with the real hostname supplied through -servername.
Switching the proxy to a mode that does not verify the origin will clear the error and leave the origin exactly as broken as it was, now serving traffic over a connection nobody is checking. What each Cloudflare SSL mode actually does sets out what changes between Flexible, Full and Full (Strict), and why the last one is the only setting that verifies the origin.
The causes that are new in 2026
Two industry changes are producing handshake failures this year in setups that worked without modification for years. Both are worth checking early, because neither shows up as anything unusual in the alert code: they present as ordinary certificate rejections and expiry failures, and the troubleshooting guides written before 2026 do not mention either.
The first is the removal of the clientAuth extended key usage from publicly trusted TLS certificates. Organisations that used a public TLS certificate as the client certificate in a mutual-TLS setup find that the server now rejects it, typically as alert 42, 46 or 49. Certum issued its last SSL/TLS certificates carrying clientAuth on 15 May 2026, DigiCert stopped on 1 May 2026 and Sectigo on 15 May 2026, so the failure arrives at renewal rather than all at once — which makes it look like a broken renewal instead of a policy change. Why public TLS certificates no longer authenticate clients covers how to confirm it and where mutual TLS should move instead.
The second is shorter certificate lifetimes. Certificates issued on or after 15 March 2026 cap at 200 days under the CA/Browser Forum Baseline Requirements, dropping to 100 days in March 2027 and 47 days in March 2029. An expiry that used to arrive once a year now arrives roughly twice, and any renewal process that depended on somebody remembering will fail sooner and more often. Expiry-driven handshake failures are the most avoidable item on this page and the one most likely to grow.
If a certificate on the failing host does need replacing rather than reconfiguring, it is worth checking what validation level it holds before reordering, because a DV certificate can be reissued in minutes while an OV or EV certificate with stale organisation data cannot. Compare what DV, OV and EV certificates need at reissue before you are doing it under time pressure.
The fixes that make it worse
Every handshake failure has a fix that makes the error message disappear in under a minute, and each one works by removing the check rather than the fault. They are worth naming, because they are the top answers on most search results for this error and they all leave the connection measurably weaker than the developer believes it to be.
| The quick fix | What it actually does | Instead |
|---|---|---|
curl -k, verify=False | Accepts any certificate from anyone, including an interceptor | Fix the chain on the server, or point the client at the right CA bundle |
| A trust-all TrustManager | Disables verification for the whole JVM process, often permanently | Import the actual issuing CA into a keystore the application points at |
| Re-enabling TLS 1.0 / 1.1 | Restores protocols deprecated across browsers since 2020 and disallowed under PCI DSS | Update the component that cannot do TLS 1.2, or isolate it behind its own endpoint |
| Importing the leaf into the trust store | Works until the next renewal, then fails again with no obvious link to the change | Serve the missing intermediate from the server, where the problem is |
| Switching the CDN off verification | Hides a broken origin and leaves the edge-to-origin hop unauthenticated | Fix the origin handshake, then return the proxy to Full (Strict) |
There is one legitimate use for the first row, and it is diagnostic. If curl -k succeeds where curl fails, you have proved the failure is verification rather than negotiation, which is genuinely useful information. The mistake is leaving the flag in the script that goes to production.
Worth checking while this is fresh
An incomplete chain and an approaching expiry both look fine in a browser right up to the moment something that is not a browser tries to connect. Both take about thirty seconds to rule out. Check what your server actually serves — the chain it sends, the names it covers and the date it expires — for the host that just failed, and for the one you renewed last month and have not tested since.
Related reading
- SSL certificate errors decoded — the browser-facing errors that appear once a handshake does complete and the certificate is then rejected.
- SSL certificate chain explained — how a chain is built, what order to install it in, and why the root is not sent.
- How SSL/TLS works — the handshake in full, for when the stage diagram above raises more questions than it answers.