Skip to content
Control Plane Labs

525 SSL Handshake Failed

Server error response defined by developers.cloudflare.com.

Last updated July 27, 2026

Common causes at a glance

  • No certificate installed on the origin, or none on the port Cloudflare dials
  • The origin does not support SNI, so it cannot select a certificate
  • No cipher suite in common between Cloudflare's edge and the origin
  • Port 443 closed or the TLS listener bound to the wrong interface

Reproduce it

openssl s_client -connect 203.0.113.10:443 -servername www.example.com </dev/null

What 525 SSL Handshake Failed means

525 and 526 are both about TLS to the origin, and the difference is where the handshake stopped. In a 525 it fails outright: no agreed cipher suite, no certificate on the port, port 443 closed, or an origin without SNI support that cannot pick a certificate for the hostname Cloudflare asked for. In a 526 the handshake succeeds and the certificate is then rejected during validation.

Both conditions must hold: the handshake fails and the zone is in Full or Full (strict) SSL/TLS mode. A Flexible zone never attempts TLS to the origin, which is why flipping from Flexible to Full is the classic way to summon a 525 out of a working site.

Where it comes from

525 is not registered with IANA and has no RFC. Cloudflare defines it in its 5xx error docs and nowhere else. RFC 9110 §15 has clients treat unrecognised 5xx codes as 500, so monitoring blurs it into generic server errors. The edge generates this page itself, with a sharper consequence than usual: because the handshake never completed, your web server may log nothing at all. TLS failures are often recorded at a different log level, or in a separate error log, so the access log looks healthy while the site is down.

What actually causes it

Cipher mismatch is the cause that shows up after someone hardens a server. Tightening the suite list to a short modern set, or disabling older protocol versions, can leave no overlap with what the edge offers; Cloudflare publishes the cipher suites it uses so you can check rather than guess. SNI matters because Cloudflare sends the visitor’s hostname, and an origin serving several sites from one IP without SNI handling presents the wrong certificate or none. Intermittent 525s point instead at resource exhaustion during handshake, since a broken setup fails every time.

How to debug it

Reproduce the handshake by hand against the origin IP with the right SNI. It tells you more than curl does, because it prints the negotiation rather than a summary:

openssl s_client -connect 203.0.113.10:443 -servername www.example.com </dev/null

No certificate chain plus an immediate handshake failure alert points at cipher or protocol mismatch. A chain belonging to a different hostname points at SNI. A refusal before any TLS means port 443 is closed and you are looking at 521. Once the handshake completes you have ruled out 525 entirely, even if the certificate is expired — that is 526’s territory. The TLS inspector shows the negotiated version and suite side by side. If you need a certificate, Cloudflare issues free Origin CA certificates.

Working examples

curl

# The handshake in full detail, with correct SNI, straight at the origin.
openssl s_client -connect 203.0.113.10:443 \
  -servername www.example.com </dev/null

# Narrow it down: does any TLS version work?
openssl s_client -connect 203.0.113.10:443 -servername www.example.com -tls1_2 </dev/null
openssl s_client -connect 203.0.113.10:443 -servername www.example.com -tls1_3 </dev/null

# curl's view of the same failure.
curl -sv --resolve www.example.com:443:203.0.113.10 \
  https://www.example.com/ 2>&1 | grep -E 'SSL|TLS|ALPN'

Python (requests)

import socket, ssl

# A raw handshake attempt. 525 means this throws before any HTTP happens.
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE  # isolate handshake failure from validation

try:
    with socket.create_connection(("203.0.113.10", 443), timeout=10) as sock:
        with ctx.wrap_socket(sock, server_hostname="www.example.com") as tls:
            print("handshake ok:", tls.version(), tls.cipher()[0])
            # Got here? Then it is not a 525 — check 526 for validation errors.
except ssl.SSLError as exc:
    print("handshake failed ->", exc)

Node (fetch)

import tls from "node:tls";

// rejectUnauthorized: false isolates handshake failure from cert validation,
// which is precisely the 525 / 526 boundary.
const sock = tls.connect(
  { host: "203.0.113.10", port: 443, servername: "www.example.com",
    rejectUnauthorized: false },
  () => {
    console.log("handshake ok:", sock.getProtocol(), sock.getCipher().name);
    console.log("not a 525 — look at 526 next");
    sock.end();
  },
);
sock.on("error", (e) => console.log("handshake failed:", e.code, e.message));

Reference and tooling

The authoritative list of assigned status codes is theIANA HTTP Status Code Registry. For a searchable copy with the semantics summarised, use theHTTP status code reference on this site. To see exactly what a server is sending, theheader inspector shows the raw status line and response headers, and thecurl builder assembles the request flags for you.

Other 5xx codes