Fix Nginx SSL_do_handshake failed (ssl3_get_record:wrong version number) when proxying to upstream

intermediate Nginx2026-06-16| Ubuntu 22.04 / Debian 11, Nginx 1.18+, Backend: Node.js/Python/Go on HTTP

Error Message

SSL_do_handshake() failed (SSL: error:1408F10B:SSL routines:ssl3_get_record:wrong version number) while SSL handshaking to upstream
#nginx#ssl#proxy_pass#https#upstream#tls

The Quick Fix (TL;DR)

This error almost always means Nginx is trying to talk HTTPS to a backend service that is only listening for HTTP. The most common fix is changing your proxy_pass protocol.

# Find this in your config:
proxy_pass https://127.0.0.1:8080;

# Change it to this:
proxy_pass http://127.0.0.1:8080;

After making the change, test your config and reload Nginx:

sudo nginx -t
sudo systemctl reload nginx

Detailed Root Cause

The error message SSL_do_handshake() failed (SSL: error:1408F10B:SSL routines:ssl3_get_record:wrong version number) is Nginx's way of saying it's confused by the data it received from the upstream (backend) server.

When you use proxy_pass https://..., Nginx starts a TLS handshake. It sends a "Client Hello" packet to the backend. If the backend is a plain HTTP server (like a local Node.js app, Gunicorn, or a Docker container not configured for SSL), it doesn't understand the TLS handshake. Instead of responding with a "Server Hello", the backend usually sends back a plain text HTTP error or a standard response.

Nginx tries to read the first few bytes of that response as a TLS version number. Since it sees the characters for "HTTP" instead of binary TLS version headers, it throws the wrong version number error. It's effectively a protocol mismatch: you're speaking encrypted Spanish to someone who only understands plain English.

Common Scenarios & Fix Approaches

Scenario 1: Accidental https:// in proxy_pass

If your Nginx handles the SSL (the "SSL termination") and the backend app is running on the same machine or a private network, you usually don't need SSL between Nginx and the backend. Many developers copy-paste a configuration and forget to change the protocol.

location /api/ {
    # Error occurs here if backend is HTTP
    proxy_pass https://backend_server;
    proxy_set_header Host $host;
}

The Fix: Change https to http. Internal traffic is generally safe if it's over a local loopback or a secure VPC.

Scenario 2: Port Confusion (Port 443 vs 80)

Sometimes you might be pointing to the right protocol but the wrong port, or vice versa. If you point Nginx to https://your-backend:80, it will try to perform a handshake on a port that expects plain text.

The Fix: Verify exactly which port your backend is listening on. If it's listening on 8080 without an SSL certificate, use http://127.0.0.1:8080. If it actually is supposed to be HTTPS, ensure the backend has its .crt and .key files correctly configured.

Scenario 3: Upstream Block Mismatch

If you use an upstream block, make sure the protocol in the proxy_pass directive matches the capabilities of the servers listed in the block.

upstream my_app {
    server 10.0.0.5:5000;
}

server {
    listen 443 ssl;
    # ... ssl config ...

    location / {
        # If the servers in 'my_app' are HTTP, this must be http://
        proxy_pass http://my_app;
    }
}

Verification Steps

To confirm exactly what's happening, you can bypass Nginx and talk to your backend directly from the server terminal using curl.

Test if the backend is HTTP:

curl -I http://127.0.0.1:8080

If this returns an HTTP 200 or 301, your backend is definitely HTTP, and your proxy_pass must use http://.

Test if the backend is HTTPS:

curl -Ik https://127.0.0.1:8080

If this works (returns headers) but the HTTP test fails, then your backend is indeed HTTPS, and the wrong version number error might be caused by an extremely old TLS version or a misconfigured certificate.

Prevention & Professional Tips

When I'm debugging these protocol mismatches, I keep a few things in mind to prevent them from hitting production:

- **Environment Variables:** Use environment variables in your deployment scripts to set the backend protocol (`BACKEND_URL=http://...` vs `https://...`).
- **Consistent Logging:** Always check `/var/log/nginx/error.log`. It provides the specific upstream IP and port that failed, which is vital when you have multiple upstreams.
- **Certificate Integrity:** If you actually intended to use SSL to the backend and it's failing, verify that your certificate files haven't been corrupted. When I'm moving certificates between servers, I often use the [Hash Generator from ToolCraft](https://toolcraft.app/en/tools/developer/hash-generator) to generate a SHA-256 hash of the `.crt` file. Comparing the hash on the source and destination ensures the file wasn't truncated or messed up during the `scp` or `rsync` transfer.

Further Reading

- Nginx Documentation on [proxy_pass](https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass)
- Understanding SSL Termination vs. End-to-End Encryption
- Debugging Nginx Upstream errors

Related Error Notes