ERR_CONNECTION_CLOSED: Causes and the Fix

· 4 min read · 42 views · Getwebup

The short answer

ERR_CONNECTION_CLOSED means the server accepted your connection and then closed it without sending any response. It is almost always the server side: a crashed PHP-FPM worker, an out-of-memory kill, a TLS handshake the server aborted, or a proxy dropping the connection. Check the error log at the moment of the failure - the cause is nearly always written there.

Not sure which error you have? Decode Chrome's code first — "This Site Can't Be Reached" in Chrome: Every Code Decoded

ERR_CONNECTION_CLOSED is the server saying nothing at all. The TCP connection was established, your request went out, and the far end closed the connection without sending a single byte of response. That is a much narrower failure than a timeout, and it usually leaves a trail in a log.

What "closed" narrows down

Getting this error tells you three things for free. The DNS resolved, so the domain is fine. The connection was accepted, so the machine is up and something is listening on that port. And the close was clean - a proper TCP FIN - which means a process decided to end the conversation rather than a network dropping it.

That eliminates DNS, routing and most firewall causes in one step. What remains is the web server, whatever sits behind it, or a proxy in between.

Rule out your own side first

It takes ten seconds and saves you from debugging a healthy server. Load the same URL on a phone with Wi-Fi off, on mobile data, with no VPN.

  • It loads - something local is closing the connection. Usually an HTTPS-inspecting antivirus, a VPN client, a corporate proxy, or a browser extension. Disable them one at a time.
  • It fails there too - it is the server. Continue below.

Separate the TLS case from the application case

Ask whether the connection survives the handshake at all:

curl -sSv https://yourdomain.com 2>&1 | tail -20
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com </dev/null

If openssl never gets to printing a certificate chain, the server is aborting during the handshake. The usual causes are a certificate file the web server cannot read after a renewal changed its permissions, a virtual host with an ssl_certificate path that no longer exists, or a configuration with no cipher in common with a modern browser.

If the handshake completes and the connection closes afterwards, the TLS layer is healthy and the application behind it is the problem.

The four server-side causes, in order of likelihood

1. A PHP worker that died mid-request

When a PHP-FPM worker segfaults or is killed, the connection to the web server closes with nothing written. Depending on configuration, that surfaces as a 502 or as a bare closed connection. The FPM log names it directly:

tail -50 /var/log/php*-fpm.log
journalctl -u php8.2-fpm --since '10 minutes ago'

A line mentioning SIGSEGV, SIGKILL, or "child exited on signal" is your answer. The two usual reasons are a memory limit being hit on a heavy request, and a PHP extension crashing on particular input.

2. The out-of-memory killer

On a small VPS, the kernel will kill the largest process rather than let the machine die. When that process is a PHP worker or MySQL, requests in flight close instantly. Check whether it has been happening:

dmesg -T | grep -i 'killed process'
grep -i 'out of memory' /var/log/syslog

If the OOM killer is active, no amount of web-server tuning will fix this - the machine needs either less concurrency, less memory per worker, or more RAM. Reducing pm.max_children so the pool cannot outgrow available memory is usually the immediate fix.

3. Worker or connection limits

A full PHP-FPM pool queues, and a queue that overflows closes connections. So does an nginx worker_connections ceiling under load. These produce the intermittent version of this error - fine at 3am, broken at peak.

grep -E 'max_children|listen.backlog' /etc/php/*/fpm/pool.d/www.conf
grep -c 'server reached pm.max_children' /var/log/php*-fpm.log

4. Something in front of the server

A CDN, a load balancer or a reverse proxy can close the connection on its own account - most often on a request that exceeds a body-size or header limit it enforces and the origin does not. If the site is behind Cloudflare or similar, test the origin directly to take the proxy out of the picture:

curl -sS -o /dev/null -w '%{http_code}\n' --resolve yourdomain.com:443:ORIGIN_IP https://yourdomain.com

Working against the origin and failing through the proxy tells you exactly which one to configure.

Reproduce it with the log open

The single most efficient way to solve this is to watch the log while you trigger the error, rather than reading it afterwards and guessing at timestamps:

tail -f /var/log/nginx/error.log /var/log/php8.2-fpm.log

Load the failing page once. Whatever appears in those two seconds is the cause. If nothing appears at all, the connection is being closed before it reaches the application, which puts you back at the TLS and proxy checks above.

Questions people actually ask