# ERR_SSL_PROTOCOL_ERROR: what it means and how to fix it

> The TLS handshake itself failed - usually a server speaking plain HTTP on port 443, or offering only protocol versions browsers now refuse.

Published: 2026-07-30 | Updated: 2026-08-05 | Canonical: https://yoping.me/guides/err-ssl-protocol-error

`ERR_SSL_PROTOCOL_ERROR` is Chrome saying the TLS handshake never
completed. This is different from an [expired
certificate](/guides/ssl-certificate-expired) - the browser never got
far enough to judge the certificate at all. The two sides failed to
agree on how to speak encrypted in the first place, and the most common
reason is that one of them wasn't speaking TLS at all.

## What does ERR_SSL_PROTOCOL_ERROR actually mean?

The browser opened a connection to port 443 and expected a TLS
handshake. What came back was something else: plain HTTP, an old
protocol version the browser refuses (SSLv3, TLS 1.0, TLS 1.1), a
reset, or bytes that don't parse as TLS. Firefox shows the same failure
as `SSL_ERROR_RX_RECORD_TOO_LONG` or "Secure Connection Failed"; curl
says "error in the TLS handshake" or "wrong version number."

"Wrong version number" is the tell worth memorizing: it almost always
means an HTTP response arrived where a TLS record was expected - a web
server answering unencrypted on 443.

## How do I see what the server is actually sending?

Two commands from any machine:

```
# What does the handshake look like?
curl -v https://example.com 2>&1 | head -20

# Ask openssl to negotiate and print the result
echo | openssl s_client -connect example.com:443 -servername example.com 2>&1 | head -15
```

Read the failure, not the summary. `wrong version number` means plain
HTTP on 443. `no protocols available` or `handshake failure` means a
protocol-version or cipher mismatch. `Connection refused` means nothing
is listening on 443 at all - a firewall or a missing `listen`
directive, not a TLS problem.

## Why is my server speaking plain HTTP on 443?

The usual suspects, in order:

- **The TLS block never loaded.** In nginx, `listen 443;` without the
  `ssl` flag serves plain HTTP on 443. It needs `listen 443 ssl;` plus
  `ssl_certificate` and `ssl_certificate_key` paths that exist.
- **A port forward or container mapping points 443 at the app's plain
  HTTP port** - the handshake reaches Node or Django directly, which
  answer in HTTP.
- **Behind Cloudflare or another CDN:** the edge terminates TLS fine,
  then forwards to an origin misconfigured as above. The error can
  appear only for the edge-to-origin leg, which surfaces as 5xx errors
  at the edge instead - test your origin's IP directly to see the truth.

After any config change: `nginx -t`, then reload, then re-run the curl
above. Servers load TLS config at startup, so an edit alone changes
nothing.

## What if the config looks right?

Then check protocol versions. Browsers require TLS 1.2 or newer;
a server pinned to TLS 1.0/1.1 by an old hardening guide now fails
modern clients:

```
openssl s_client -connect example.com:443 -tls1_2 </dev/null
```

If that fails while the plain `s_client` line above also fails, update
`ssl_protocols TLSv1.2 TLSv1.3;` (nginx) or the equivalent. And if
visitors report the error while every test of yours passes, suspect
their side: corporate proxies and antivirus "HTTPS scanning" intercept
handshakes, and a wrong system clock breaks TLS in confusing ways.

## How do I catch this before visitors do?

A broken handshake is a full outage that never shows in your server's
access log - requests die before a request line exists. It takes an
outside client to see it. YoPingMe's HTTP and SSL checks perform the
full handshake from both of your check regions on every run, so
a bad reload or a protocol misconfiguration pages you by email,
webhook, or Slack instead of waiting for a user's screenshot.
[Ten monitors are free](https://app.yoping.me/signup), and the first check
runs while you sign up.
