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/nullIf 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/syslogIf 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.log4. 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.comWorking 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.logLoad 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.