Nginx Error: 'peer closed connection in SSL handshake while SSL handshaking' — Cause, Fix, and Troubleshooting Guide
Understand nginx 'peer closed connection in SSL handshake while SSL handshaking' — usually benign probes, HTTP-to-HTTPS, LB health checks. When it is real.
- #nginx
- #web-server
- #troubleshooting
- #tls
Stuck on this NGINX error? Get the free incident triage checklist
A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.
What this error means
This log line means a client opened a TCP connection to your TLS port and then closed it partway through the TLS handshake — before it completed — without a proper TLS close_notify. nginx logs it at [info] level (it is informational, not a failure of nginx) with no request: field, because no HTTP request was ever exchanged. The overwhelming majority of these lines are harmless: health checkers, port scanners, TCP load-balancer probes, and clients that spoke plain HTTP to a TLS port then hung up.
2026/07/12 08:41:16 [info] 1188#1188: *20714 peer closed connection in SSL handshake while SSL handshaking, client: 203.0.113.7, server: 0.0.0.0:443
Impact is usually zero — no user request failed. The problem is signal: these lines can drown out real handshake errors, and occasionally they do indicate a genuine misconfiguration (wrong SNI, a broken TLS-terminating load balancer, or a client that cannot complete the handshake).
Symptoms at the client
- Frequent
[info]lines:peer closed connection in SSL handshake while SSL handshaking, client: <ip>, server: 0.0.0.0:443with norequest:field. - The source IPs are often your load balancer, monitoring system, or Kubernetes node IPs (health checks), or random internet IPs (scanners).
- Volume is steady and correlates with health-check intervals, not with user traffic or errors.
- Real users are unaffected — no corresponding
5xxinaccess.log, no user complaints. - Occasionally clusters from a single real client that genuinely cannot complete TLS (then it matters).
- Sometimes paired with
client sent plain HTTP request to HTTPS port(error400) from the same sources.
Trust and cipher causes
- TCP-only health checks / probes — a load balancer, uptime monitor, or Kubernetes
tcpSocketprobe opens the socket to confirm the port is listening, then closes it before doing any TLS. Entirely benign. - Plain HTTP sent to the TLS port — a client or misconfigured checker connects with
http://to:443; nginx expects a ClientHello, the client sendsGET / HTTP/1.1or just disconnects, leaving this line (often alongside a400). - Port scanners and bots — internet background noise (Shodan-style scanners, vuln scanners) connects and drops constantly on any public
:443. - SNI mismatch on a strict default server — a client connects without SNI or with an unknown SNI; if your default server is a
ssl_reject_handshake on;catch-all or otherwise rejects, some clients bail mid-handshake. - A TLS-terminating LB in front doing its own probes — the upstream LB health-checks your nginx TLS listener at Layer 4 and closes early; noise, not failure.
- Genuine client-side abort — a real client with a broken TLS stack, an aggressive timeout, or a middlebox that resets the handshake. This is the rare case that actually needs investigation.
Inspecting the TLS handshake
First measure the volume and the source IPs — this tells you benign-noise vs. real problem faster than anything else:
# Count occurrences and rank the client IPs producing them
grep 'peer closed connection in SSL handshake' /var/log/nginx/error.log \
| grep -oE 'client: [0-9.]+' | sort | uniq -c | sort -rn | head
Cross-check whether those same IPs ever complete a real request (if they never appear in access.log, they are almost certainly probes):
# Pick a top offender IP and see if it made any real HTTP request
grep '203.0.113.7' /var/log/nginx/access.log | head
Look for the tell-tale plain-HTTP-to-TLS companion error:
grep 'plain HTTP request to HTTPS port' /var/log/nginx/error.log | wc -l
Confirm your own handshake actually succeeds (so you know nginx’s TLS is healthy and the noise is external):
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>&1 | grep -E 'Verify return code|Protocol|Cipher'
Watch a live capture briefly to see who is connecting and dropping (read-only):
sudo ss -tn state syn-recv '( sport = :443 )' # half-open handshakes in flight
sudo timeout 20 tcpdump -ni any 'tcp port 443 and (tcp[tcpflags] & tcp-fin != 0)' -c 50
If you suspect health checks, list the prober IPs (your LB/monitoring/node CIDRs) and confirm they match the top offenders from the first command.
The fix
In most cases there is nothing to fix in TLS — the goal is to (a) confirm it is benign and (b) stop it from polluting your logs. If it is health checks or probes, silence just this class of message by raising nginx’s error-log threshold above info (the default is already error, so this only bites people who set error_log ... info;):
# Log real problems, drop info/notice noise like handshake-abort probes
error_log /var/log/nginx/error.log warn;
Give TCP/L4 health checkers a lightweight endpoint so they stop half-opening the TLS port. Point the load balancer/monitor at a plain-HTTP status location instead of probing :443 at Layer 4:
server {
listen 8080; # internal-only health port, plain HTTP
server_name _;
location = /healthz {
access_log off;
return 200 "ok\n";
}
}
If clients are hitting :443 with plain HTTP, give them a clean answer instead of a dropped handshake by handling the error code on the TLS server:
server {
listen 443 ssl;
server_name example.com;
# ... ssl_certificate / ssl_certificate_key ...
# 497 = "HTTP request sent to HTTPS port"; redirect them to https
error_page 497 =301 https://$host$request_uri;
}
To stop unknown-SNI/default-server clients from lingering, make the default server reject cleanly (nginx 1.19.4+), which is explicit and cheap:
server {
listen 443 ssl default_server;
ssl_reject_handshake on; # cleanly reject connections with no/unknown SNI
}
Only if diagnosis shows a real client failing (it appears in access.log attempts, comes from a user network, and never completes) do you treat it as a genuine handshake problem — then pivot to protocol/cipher and certificate diagnosis. Validate and reload after any config change:
sudo nginx -t && sudo systemctl reload nginx
Certificate lifecycle
- Treat this as
[info]noise by default — confirm with the IP-frequency command before spending time on it; the absence of matchingaccess.logentries is the strongest “benign” signal. - Do not run
error_log ... info;in production long-term; it makes these lines flood the log and hides real errors. Keep it atwarn/error. - Route L4/TCP health checks to a dedicated plain-HTTP port so they never half-open your TLS listener.
- Always test with SNI (
-servername) when validating; a handshake that fails only without SNI points at a default-server policy, not a broken cert. - Distinguish this from
SSL_do_handshake() failed(nginx-side negotiation/cipher failure) and fromcertificate verify failed(trust failure) — those are real, this usually is not. - Baseline the normal rate and alert only on anomalous spikes; a sudden jump can mean a broken LB probe or a client population change. Track the rate on your monitoring dashboard.
Related TLS errors
- Nginx Error: Ssl Do Handshake Failed — the real negotiation-failure counterpart when the handshake fails on nginx’s side rather than the peer simply hanging up.
- Nginx Error: Ssl Handshake Certificate — certificate/trust handshake failures to rule out when a client genuinely cannot connect.
- Nginx Error: Recv Failed Connection Reset Upstream — a related “peer went away” reset, but on the upstream connection instead of the client handshake.
See the NGINX category for more guides.
Fixed it? Get 500 NGINX & DevOps AI prompts — free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.
Did this fix your issue?
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.