ERR_CONNECTION_RESET & ERR_EMPTY_RESPONSE: Causes and the Fix
Your site was loading a second ago, and now the browser is throwing "ERR_CONNECTION_RESET" or "ERR_EMPTY_RESPONSE" instead of a page. No SSL warning, no 500 error, nothing in the response body to work with - just a connection that started and then got cut off. That's a different failure mode from a timeout or a refused connection, and it points to a different set of causes. Here's how to tell them apart and actually fix it.
What these errors mean (and how they differ from a timeout)
Both errors happen after the browser successfully connects to your server - that's the key difference from ERR_CONNECTION_TIMED_OUT or ERR_CONNECTION_REFUSED, where the connection never opens in the first place. Here, the TCP handshake completes, the browser sends the request, and then something kills the connection mid-flight instead of sending a clean response.
- ERR_CONNECTION_RESET - the connection was open, and then something sent a TCP RST (reset) packet, slamming the door shut. This is usually deliberate: a firewall rule, a security module, or the backend process dying abruptly.
- ERR_EMPTY_RESPONSE - the connection closed cleanly but with zero bytes of HTTP response. Chrome's way of saying "I got nothing back to show you." Common when a reverse proxy (Nginx, Cloudflare) gives up waiting on a backend that never replies.
They often share the same root cause and show up interchangeably depending on exactly where in the chain the connection got killed - proxy layer vs. origin server vs. network device in between.
Symptom checklist
- It happens on specific pages (checkout, search, an image-heavy gallery, an API call) but not the homepage
- It's intermittent - works on refresh, or works for some visitors and not others
- It started right after a plugin update, a security rule change, or a traffic spike
curl -vagainst the URL shows the connection closing before any headers come back- Large uploads or long-running requests (imports, exports, big API calls) are the ones that fail
Common causes, most likely first
1. ModSecurity or another WAF is killing the connection mid-request
When ModSecurity (or Imunify360, Wordfence's firewall, a custom WAF rule) detects something it doesn't like partway through a request - a suspicious POST body, an oversized upload, a pattern match on the URL - some configurations drop the connection outright instead of returning a clean 403. That reads as ERR_CONNECTION_RESET to the browser, not a blocked-page message.
Check the ModSecurity audit log for a hit around the same timestamp:
grep -i "$(date +%d/%b/%Y)" /usr/local/apache/logs/modsec_audit.log | tail -100
# or on a cPanel server via WHM: WHM >> ModSecurity Tools >> Hits
If you find a rule ID firing on the exact request that's failing, whitelist that rule for that specific path rather than disabling ModSecurity entirely.
2. PHP-FPM worker crashed or hit the memory limit mid-response
If a PHP process dies while it's already started sending output - an OOM kill, a segfault in a compiled extension, a fatal error after headers were partially sent - the connection just stops instead of finishing. This is common on memory-heavy operations: large WooCommerce exports, PDF generation, image processing with Imagick.
tail -50 /var/log/php-fpm/error.log
dmesg | grep -i "oom-killer\|out of memory" | tail -20
If dmesg shows the kernel's OOM killer targeting php-fpm around the failure time, the fix is either raising memory_limit in php.ini, adding swap on the VPS, or - better - fixing whatever script is trying to load an unreasonable amount of data into memory at once.
3. Nginx or PHP-FPM ran out of workers under load
When every PHP-FPM worker is busy and a new request comes in, what happens next depends on your pm.max_children and the process manager's queue behavior. If the queue fills up too, Nginx can't get a response from upstream and closes the connection - ERR_EMPTY_RESPONSE on the client side. Check the Nginx error log:
tail -100 /var/log/nginx/error.log
# look for lines like:
# upstream prematurely closed connection while reading response header from upstream
That exact line - "upstream prematurely closed connection" - is the smoking gun. It means Nginx opened the connection to PHP-FPM fine, but PHP-FPM closed it before sending a full response.
4. A timeout mismatch between the proxy and the backend
If Cloudflare, Nginx, or a load balancer has a shorter timeout than the PHP script actually needs, the proxy gives up and closes the connection while PHP is still working - the client sees ERR_EMPTY_RESPONSE even though the backend would have eventually responded. This is the classic cause for slow reports, large CSV imports, or bulk API operations.
Check and align these three settings so the proxy always waits at least as long as the backend:
| Layer | Setting | Typical default |
|---|---|---|
| PHP | max_execution_time | 30s |
| Nginx (as proxy) | proxy_read_timeout | 60s |
| Cloudflare | Origin response timeout (Enterprise) / 100s hard cap (all plans) | 100s |
If PHP is allowed to run for 120 seconds but Nginx only waits 60, Nginx will always cut the connection first. Raise proxy_read_timeout and proxy_send_timeout in your Nginx site config to match or exceed PHP's limit.
5. Fail2Ban or CSF/LFD banned the visitor's IP mid-session
If a visitor triggers a rate-limit or brute-force rule (too many login attempts, too many rapid requests, a bot crawling aggressively from a shared IP) partway through their session, Fail2Ban or CSF can insert a firewall rule that resets the existing connection, not just block new ones. Check for a recent ban matching the affected IP:
csf -g VISITOR_IP
# or
fail2ban-client status apache-badbots
grep VISITOR_IP /var/log/fail2ban.log
If it's a legitimate visitor caught by an overly aggressive rule (shared office IP, a monitoring service, an API integration), whitelist that IP or loosen the trigger threshold.
6. Cloudflare SSL mode mismatch
If Cloudflare's SSL/TLS mode is set to "Full" but your origin doesn't actually have a valid certificate (self-signed, expired, or none at all), Cloudflare can reset the connection rather than showing a clean error - this most often shows as ERR_CONNECTION_RESET right after enabling Cloudflare or after a certificate expired unnoticed. Confirm with:
curl -vI https://your-origin-ip -H "Host: yourdomain.com" -k
If that fails or shows a cert error, fix AutoSSL on the origin first, then verify Cloudflare's SSL mode matches what the origin can actually serve.
Step-by-step diagnosis
- Reproduce it with
curl -vfrom a shell that has direct access to the origin (bypass Cloudflare with--resolve yourdomain.com:443:ORIGIN_IPif needed) and note exactly where the output stops. - Pull the timestamp of a failed request and grep the Nginx/Apache error log, the ModSecurity audit log, and PHP-FPM's error log for that same second.
- Check
dmesgfor OOM kills around the same time. - Check CSF/Fail2Ban logs for a ban against the affected IP.
- If it only happens through Cloudflare and not when hitting the origin IP directly, the problem is in the Cloudflare-to-origin leg - check SSL mode and timeout settings there.
Prevention
- Set
proxy_read_timeout,proxy_send_timeout, and PHP'smax_execution_timeto consistent values across every layer a request passes through. - Monitor PHP-FPM's error log and set up an alert on OOM kills so a memory-hungry script gets caught before it becomes a recurring reset.
- Review ModSecurity hits weekly in WHM instead of only when something breaks - a rule that's about to start blocking real traffic usually shows a few near-misses first.
- Keep AutoSSL renewal working on the origin so a Cloudflare "Full (strict)" setup never silently starts resetting connections when a cert lapses.
Frequently asked questions
Is ERR_CONNECTION_RESET the same as ERR_CONNECTION_REFUSED?
No. ERR_CONNECTION_REFUSED means the connection was never established - nothing was listening on the port. ERR_CONNECTION_RESET means the connection opened successfully and then got forcibly closed mid-request, usually by a firewall, WAF, or a crashed backend process.
Why does the error only happen on some pages or for some visitors?
Because the usual causes are request-specific: a WAF rule triggered by a particular payload, a memory-heavy script on one page, or a rate-limit that only certain IPs hit. If it happened on every single request from everyone, you'd more likely see the whole site down, not an intermittent reset.
Could this be a problem on the visitor's end, not my server?
It's possible - an antivirus proxy, a corporate firewall, or a flaky ISP connection can also cause resets. Rule that out first by reproducing the issue yourself with curl -v from a different network. If curl reproduces it consistently, the problem is server-side.
I use Cloudflare - how do I check the origin directly without going through it?
Use curl with --resolve to point the domain at your origin IP while still sending the correct Host header and SNI: curl -vI https://yourdomain.com --resolve yourdomain.com:443:YOUR_ORIGIN_IP. If the error disappears, the issue is between Cloudflare and your origin, not your app itself.
Will raising PHP's memory_limit fix this permanently?
It can mask the symptom, but if a script's memory use keeps growing (a bad loop, an ever-larger export, no pagination on a big query) you'll hit the new limit eventually too. Treat a memory_limit increase as a stopgap and profile the actual script if the same operation keeps causing OOM kills.