Skip to content
🎉 Launch sale:50% off everything over $22 — automatically applied at checkout· ends Aug 2Shop the sale →
DevOps AI ToolKit
Newsletter
All guides
AI for NGINX By James Joyner IV · · 8 min read Last reviewed Jul 2026

Nginx Error: 'peer closed connection in SSL handshake while SSL handshaking' — Cause, Fix, and Troubleshooting Guide

Quick answer

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
Free toolkit

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:443 with no request: 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 5xx in access.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 (error 400) from the same sources.

Trust and cipher causes

  • TCP-only health checks / probes — a load balancer, uptime monitor, or Kubernetes tcpSocket probe 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 sends GET / HTTP/1.1 or just disconnects, leaving this line (often alongside a 400).
  • 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 matching access.log entries 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 at warn/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 from certificate 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.

See the NGINX category for more guides.

Free download · 368-page PDF

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?

Free download · 368-page PDF

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.