SSL handshake failed

The client and server could not agree on a TLS version, cipher or certificate. Four root causes, each identified by its alert number, measured against live edges and badssl.com.

"SSL handshake failed" means the client and the server could not agree on a protocol version, a cipher, or a certificate before any HTTP was exchanged, so the connection was abandoned. It has four root causes, and the fastest way to tell them apart is the TLS alert number the failing side sent, which every client will show you if you ask.

The phrase is a catch-all. Browsers, curl, Java, Node and Cloudflare all print it (or a cousin of it) for failures that have nothing in common except the moment they happen, so the first job is to sort the message into one of two families. Everything below was measured on 2026-09-06 against badssl.com's deliberately broken hosts and against live production edges.

Domainee is a custom domains API for SaaS with a native MCP server — 50 domains and 100 GB free.

Two families hiding under one message

A TLS handshake can fail in two places. Either the two sides never agree on how to talk (version, cipher, or which hostname is meant), or they agree and then the client rejects the certificate it was handed. Clients report the two families differently, and the split is the most useful thing on this page:

What actually happenedcurl exit codeChromeFirefoxJava
Handshake refused (version, cipher, SNI)35ERR_SSL_VERSION_OR_CIPHER_MISMATCH, ERR_SSL_PROTOCOL_ERRORSSL_ERROR_NO_CYPHER_OVERLAP, SSL_ERROR_HANDSHAKE_FAILURE_ALERTSSLHandshakeException: Received fatal alert: handshake_failure
Certificate rejected after a good handshake60ERR_CERT_DATE_INVALID, ERR_CERT_COMMON_NAME_INVALID, ERR_CERT_AUTHORITY_INVALIDSEC_ERROR_EXPIRED_CERTIFICATE, SSL_ERROR_BAD_CERT_DOMAIN, SEC_ERROR_UNKNOWN_ISSUERSSLHandshakeException: PKIX path building failed

The curl codes are the cleanest test. Exit 35 is a real handshake failure; exit 60 means the handshake completed and verification failed, which is a certificate problem with a different fix. Cloudflare's 525 belongs to the first family and its 526 to the second, which is why they are separate codes.

Read the alert number first

When the refusing side is polite, it sends a TLS alert before closing. OpenSSL prints it as SSL alert number N, and the number is the diagnosis:

AlertNameMeans
40handshake_failureNo cipher suite in common, or a parameter the server will not accept
42-46bad_certificate, certificate_revoked, certificate_expired, certificate_unknownThe client's certificate was rejected (mutual TLS only)
48unknown_caIssuer not trusted by whoever sent the alert
70protocol_versionNo TLS version in common
71insufficient_securityThe offered ciphers are all too weak
112unrecognized_nameThe server does not know the SNI hostname
116certificate_requiredServer demanded a client certificate and got none

Two of those came up repeatedly in testing, and one failure mode sends no alert at all.

Cause 1: no protocol version in common (alert 70)

Forcing TLS 1.0 or 1.1 against modern edges produced the same result every time:

$ openssl s_client -connect domainee.dev:443 -servername domainee.dev -tls1
tlsv1 alert protocol version ... SSL alert number 70

vercel.com and github.com answered identically. cloudflare.com did not: it negotiated TLS 1.0 with ECDHE-RSA-AES128-SHA, because Cloudflare's minimum TLS version defaults to 1.0 and the site keeps that default. So an old client that works on one site and fails on another is not misconfigured on the failing site; the failing site is the one with a sane minimum.

The reverse case is a server stuck on an old version. tls-v1-0.badssl.com only speaks TLS 1.0, and macOS curl (built on SecureTransport) returned HTTP 200 from it, while Chrome, which dropped TLS 1.0 and 1.1 in version 84, shows ERR_SSL_VERSION_OR_CIPHER_MISMATCH. Same server, one client succeeds, one fails.

A third shape of alert 70 is not a TLS problem at all. Pointing https:// at a port that speaks plain HTTP (openssl s_client -connect example.com:80) produced tlsv1 alert protocol version on this machine and SSL_ERROR_RX_RECORD_TOO_LONG in Firefox, because the client tried to parse an HTTP response as a TLS record. If you see that, check the port before anything else.

Cause 2: no cipher in common (alert 40)

Offering a single obsolete cipher to a modern edge is the canonical alert 40:

$ openssl s_client -connect cloudflare.com:443 -servername cloudflare.com -tls1_2 -cipher DES-CBC3-SHA
sslv3 alert handshake failure ... SSL alert number 40

Again the client decides. rc4.badssl.com offers only RC4, and macOS curl connected with ECDHE-RSA-RC4-SHA and got a 200; every current browser refuses RC4 outright. dh480.badssl.com uses a 480-bit Diffie-Hellman group, and there the client aborted on its own with bad dh p length and never sent an alert, which is the failure mode that shows up in logs as a connection reset with no explanation.

The practical rule: if only some clients fail, list which ones. Old Java runtimes, embedded devices, and anything pinned to a single cipher are the usual suspects, and the fix is on their side, not by re-enabling RC4.

Cause 3: the edge does not know the hostname

This is the one a custom domain platform meets constantly, and the one the generic guides skip. Every request to a shared IP carries the intended hostname in the SNI extension, and the edge uses it to pick a certificate. What happens when the name is missing or unknown depends entirely on the edge, measured live:

EdgeNo SNI at allSNI for a hostname it does not host
VercelServes a placeholder certificate, no-sni.vercel-infra.comCloses the connection mid-handshake. curl reports exit 35, SSL_ERROR_SYSCALL, zero bytes read
Railway (the edge in front of this site)Serves its wildcard, *.up.railway.appServes the same wildcard: handshake succeeds, then a name mismatch, curl exit 60
CloudflareServes the certificate of whichever site the IP belongs toServed a certificate for a different site sharing the IP, then a name mismatch

So an unknown hostname produces a genuine "SSL handshake failed" on one platform and a certificate-name error on the other two. If your customer says handshake failed and your customer is on your Vercel-style edge, the most likely explanation is that their CNAME is already pointing at you and their certificate does not exist yet. HTTP-01 cannot issue before DNS resolves to you, so there is always a window; the runbook for customer domain issues walks the order of checks.

Two related traps. A hostname that resolves to your old IP because DNS has not propagated gets whatever certificate lives at that old address, which is the "it works for me, fails for them" pattern; the propagation checker shows which resolvers still hold the old answer. And a client that sends no SNI at all (very old Java, some monitoring probes, Python 2 era libraries) will get the fallback certificate everywhere, so their failure is real but not yours to fix.

Cause 4: the certificate was rejected

The second family. The handshake itself completed and the client refused what it saw:

Test hostcurl saidopenssl verify code
expired.badssl.comcertificate has expired10
wrong.host.badssl.comno alternative certificate subject name matches target host name62
self-signed.badssl.comself signed certificate18
untrusted-root.badssl.comself signed certificate in certificate chain19
incomplete-chain.badssl.commacOS curl: 200; OpenSSL: unable to get local issuer certificate20, then 21

The incomplete-chain row is the one that generates support tickets. The server sent only its leaf certificate and not the intermediate that links it to a trusted root. Browsers and macOS fetch the missing intermediate themselves using the AIA URL in the certificate, so the site looks fine in Chrome and Safari; OpenSSL-based clients, Android, and most server-side HTTP libraries do not, and fail. The SSL certificate checker shows the chain as served, which is what those clients see.

Each rejection has its own page: expired, name mismatch, self-signed, and an issuer that is not a trusted CA. A SAN certificate that simply omits the hostname is the most common cause of the second one on multi-domain setups.

Cloudflare 525 is the same failure, one hop back

Cloudflare's Error 525: SSL handshake failed is not about the browser at all. Cloudflare accepted the browser's request and then failed its own handshake with your origin, so everything above applies with Cloudflare as the client. It only happens in Full or Full (strict) mode, and Cloudflare's documentation lists exactly four origin-side causes: no valid certificate installed, port 443 not open, no SNI support at the origin, and no cipher suite in common with Cloudflare.

Two details from the same document worth knowing. Intermittent 525s are almost always the origin resetting connections under load rather than a bad certificate, and the place to look is the origin's TLS error log at the matching timestamps. And Cloudflare's Origin Analytics reports these as originResponseStatus of 0, which is the only place a 525 shows up separately from a 5xx the origin actually sent. 526 is the second family: the handshake worked and Cloudflare rejected the origin certificate, so it is fixed with a valid certificate or an Origin CA certificate, never with cipher changes.

A diagnosis order that works

  1. Get the exit code. curl -v https://host/ and read the last lines. Exit 35 is a handshake refusal, 60 is verification. Everything else on this page branches from that.
  2. Get the alert. openssl s_client -connect host:443 -servername host prints SSL alert number N when there is one. No alert and zero bytes read means the server closed the socket, which points at an unknown SNI name or a client-side abort.
  3. Compare with and without SNI. Run the same command with -servername set to a name the server definitely hosts. If that works, the hostname is the problem, not TLS.
  4. Look at the chain, not the leaf. openssl s_client -showcerts lists what the server actually sends; one certificate where two are expected is the incomplete chain.
  5. Test from a second client. The measurements above show macOS curl accepting three configurations that browsers reject. A failure that only one client reproduces is that client's policy.
  6. Check the port. Alert 70 against a port that serves plain HTTP is a routing mistake; the website status checker and HTTP header checker will show what actually answers there.

In a custom domain platform

If you terminate TLS for customer hostnames, you are the edge in the table above, and you choose what an unknown name gets. Serving a clearly named fallback certificate (as Vercel does for the no-SNI case) turns a silent handshake drop into an error that names the problem, and it lets your own probes distinguish "not provisioned yet" from "broken". Keep the minimum at TLS 1.2 and let the alert 70s happen; the clients that send them are the ones you do not want. And alert on failed renewals rather than on approaching expiry, because a lapsed renewal surfaces as this error on the customer's domain, not yours. The guide to how SSL works for custom domains covers provisioning, and monitoring certificates at scale covers catching the lapse before the customer does.

FAQ

Is "SSL handshake failed" a client problem or a server problem? Either, and the alert number tells you which side refused. Alert 70 or 40 sent by the server means the client offered versions or ciphers the server will not accept; the same alerts from the client mean the reverse. A certificate rejection is always the client's verdict on what the server sent, so the fix is on the server.

Why does the site work in Chrome but fail in my script or app? Usually an incomplete certificate chain. Browsers fetch a missing intermediate on their own; OpenSSL-based clients, Android and most HTTP libraries do not and report unable to get local issuer certificate. Serve the full chain and both work.

What does SSL alert number 40 mean? handshake_failure: no cipher suite or key-exchange parameter in common. In testing it came from offering a single obsolete cipher to a modern edge. Check what the client offers before touching the server's cipher list.

What does SSL alert number 70 mean? protocol_version: no TLS version in common. Modern edges send it to TLS 1.0 and 1.1 clients. It also appears when https:// is pointed at a port speaking plain HTTP, so confirm the port before assuming a TLS misconfiguration.

Why do my customers get a handshake error before their custom domain is set up? Their CNAME already resolves to your edge but their certificate has not been issued yet, so the edge has nothing to present for that SNI name. Some edges close the connection at that point, which surfaces as "SSL handshake failed". Once the certificate exists the error clears without any change on the customer's side.

Is Cloudflare 525 the same as a browser SSL handshake failure? Same mechanism, different client. Cloudflare is the client and your origin is the server. The four documented causes are a missing origin certificate, port 443 closed, no SNI support at the origin, and no cipher in common with Cloudflare.

Want this handled for you? Start free with Domainee — 50 custom domains + 100 GB bandwidth, no card.