What the browser is telling you

Chrome’s error page reads “example.com didn’t send any data” with the code ERR_EMPTY_RESPONSE (internally net error -324). Edge and other Chromium browsers show the same thing.

The sentence is accurate. The browser opened a connection, sent its request, and the other end closed the connection before a single byte of response came back. No status line, no headers, nothing.

That rules a few things out straight away:

  • DNS worked. The browser had an IP address to connect to.
  • The port is open. A closed port gives ERR_CONNECTION_REFUSED, and a firewall silently dropping packets gives ERR_CONNECTION_TIMED_OUT.
  • On HTTPS, the TLS handshake generally completed too. A connection that dies during the handshake usually shows up as ERR_CONNECTION_CLOSED or an SSL error instead.

What’s left is that whatever accepted your connection decided, or was forced, to hang up without answering. If it hangs up with a normal close you get this error; if it slams the connection with a TCP reset, Chrome shows ERR_CONNECTION_RESET. Same family of problem, slightly different exit.

Note “whatever accepted your connection”. That isn’t necessarily the web server. It can be a load balancer, a proxy on your company network, or antivirus software on your own machine that intercepts web traffic.

curl reports the same condition in its own words, which makes it the best tool for reproducing this:

$ curl -v http://example.com/
*   Trying 203.0.113.10:80...
* Connected to example.com (203.0.113.10) port 80
> GET / HTTP/1.1
> Host: example.com
> ...
* Empty reply from server
curl: (52) Empty reply from server

The exact lines vary between curl versions, but curl: (52) Empty reply from server is the one to look for.

First, find out whose problem it is

If you’re visiting a site you don’t run, spend two minutes on this before anything else.

  1. Try another network. Load the page on your phone with Wi-Fi turned off. If it works there, the site is up and something on your network or computer is involved.
  2. Try a private window. If the site loads in a private or incognito window but not in your normal one, the difference is almost always cookies (see the section on oversized requests below). Clear the cookies for that site only: in Chrome, click the icon left of the address bar, open the site settings and delete its data.
  3. Turn off VPNs, proxies and antivirus web protection one at a time and reload. Software that inspects web traffic sits in the middle of the connection, and when it fails it can close your connection without passing anything back.
  4. Check from outside. Run the HTTP check on the URL. It requests the page from our servers, not from your network. If it gets a normal status code, the site is fine and the problem is local to you. If it also gets nothing back, the site is broken for everyone and only its owner can fix it.

If the site is behind Cloudflare or another CDN, visitors rarely see ERR_EMPTY_RESPONSE for an origin problem. The CDN holds the visitor’s connection, so an origin that hangs up shows as a CDN error page instead (Cloudflare uses 520 for an empty or unreadable origin response). Seeing a bare ERR_EMPTY_RESPONSE on a CDN-fronted site usually points back at the visitor’s side or at the CDN’s configuration for that hostname.

If you run the site: the usual causes

1. nginx return 444

This one is deliberate, which is why it’s easy to forget. nginx has a non-standard status code 444 that means “close the connection without sending a response”. Browsers show it as ERR_EMPTY_RESPONSE.

It appears in two common snippets:

  • A default server block that drops requests for unknown hostnames, so scanners hitting the bare IP get nothing.
  • Bot or country blocking rules, such as if ($http_user_agent ~* ...) { return 444; }.

The trouble starts when a real hostname falls into the default block (you added a domain but not to server_name, or a new www record points at the server) or when a blocking rule matches real visitors.

sudo nginx -T 2>/dev/null | grep -n 'return 444'
sudo grep '" 444 ' /var/log/nginx/access.log | tail

nginx logs these requests with status 444, so the access log tells you exactly which requests were dropped and which Host they carried. The fix is to add the hostname to the right server_name, or narrow the rule.

2. Plain HTTP sent to an HTTPS port

If a browser sends http://example.com:443/ or http://localhost:8443/, the server receives a plaintext request where it expected a TLS handshake. What happens next depends on the server:

  • nginx and Apache detect this and answer with a 400 error page (“The plain HTTP request was sent to HTTPS port” in nginx). You see an error page, not an empty response.
  • Servers that don’t detect it simply close the socket. Depending on how they close it, Chrome shows ERR_EMPTY_RESPONSE or ERR_CONNECTION_RESET.

This turns up in local development (a dev server switched to HTTPS while the bookmark still says http://), in port forwarding rules that map an HTTP port to a TLS listener, and in redirects that build a URL with the wrong scheme and port.

The reverse mistake, https:// to a port that only speaks plain HTTP, usually gives ERR_SSL_PROTOCOL_ERROR instead, because the browser receives a plaintext HTTP error where it expected a TLS reply.

Test both schemes against the port:

curl -v http://example.com:8443/
curl -vk https://example.com:8443/

Whichever one gets a real response tells you what the port actually speaks.

3. The process holding the connection died

If the process that accepted the connection crashes, gets killed or restarts while handling the request, the kernel closes its sockets and the browser gets nothing.

There’s a useful detail here. If a reverse proxy such as nginx sits in front of your app and the app dies, the proxy is still holding the browser’s connection and answers with a 502 Bad Gateway. So an empty response usually means the thing that crashed was the front-most process: an app server exposed directly (Node, Gunicorn, a framework’s built-in server), or the proxy itself.

Look for crashes and kills at the time of the failed request:

journalctl -u myapp --since "30 min ago"
systemctl status myapp            # restart counts, last exit status
sudo dmesg -T | grep -i -E 'out of memory|killed process'
docker ps -a                      # "Exited (137)" means SIGKILL, often the OOM killer

If it fails only under load or only on heavy pages, memory is the first suspect. Fix the leak or raise the limit; restarting on a timer only hides it.

4. Docker port published, app listening on 127.0.0.1

A classic in local development. The app inside the container listens on 127.0.0.1:3000, which inside a container means “only reachable from inside this container”. You publish the port with -p 3000:3000 and open http://localhost:3000.

From the same machine, Docker’s port forwarding accepts the connection and then has nowhere to forward it, so curl reports Empty reply from server or Connection reset by peer, and the browser shows ERR_EMPTY_RESPONSE or ERR_CONNECTION_RESET. From another machine you are more likely to get connection refused.

The fix is to make the app listen on all interfaces inside the container: --host 0.0.0.0 for most dev servers, app.run(host="0.0.0.0") in Flask, server.listen(3000, "0.0.0.0") in Node. Check with docker exec <container> ss -ltn (if the image has ss) that the address is 0.0.0.0:3000 and not 127.0.0.1:3000.

5. The request is too big

Huge cookies are the usual culprit. An app that keeps adding cookies, or a session cookie that grows with every visit, eventually pushes the request headers past what the server or load balancer accepts. Some servers answer with a 400 or 431 error page. Others, and some load balancers and security appliances, close the connection instead.

The tell is “works in a private window, fails in my normal browser”. To confirm from the command line, send an oversized cookie and see what comes back:

curl -v -H "Cookie: test=$(head -c 12000 /dev/zero | tr '\0' a)" https://example.com/

If that returns an empty reply while the same request without the cookie works, you’ve found the limit. Either stop the cookies from growing (the real fix) or raise the header size limit on every layer: nginx large_client_header_buffers, Apache LimitRequestFieldSize, and your load balancer’s equivalent.

Large uploads can trigger the same thing when a layer refuses the body by closing the connection rather than answering 413.

6. A timeout that hangs up instead of answering

Some application servers, proxies and network devices have an idle or request timeout whose action is to close the socket, not to send a 503 or 504. Slow endpoints (exports, reports, big searches) are the ones that hit it.

The pattern is easy to spot: the request always fails after about the same amount of time. Measure it:

curl -sS -o /dev/null -w '%{http_code} %{time_total}s\n' https://example.com/slow-report

If it dies at 60.0 seconds every time, look for a 60-second timeout in every layer the request passes through, then either raise the timeout or, better, make the slow work asynchronous so the request returns quickly.

7. Security software that cuts connections

A WAF, intrusion prevention system or host firewall that inspects requests can decide, after reading yours, to close the connection. Depending on how it does that you get an empty response or a reset. If only some URLs or some query strings fail, check the security tool’s logs for blocks at that time before you dig into the application.

Reproduce it against the origin directly

When a load balancer or CDN sits in front, test the server behind it so you know which layer is hanging up. --resolve makes curl connect to a specific IP while still sending the right hostname:

curl -v --resolve example.com:443:203.0.113.10 https://example.com/

To see what the server does with a hostname it doesn’t know, which is how you catch a default-server return 444:

curl -v -H "Host: not-configured.example" http://203.0.113.10/

The port check confirms from outside that the port is open and accepting connections, which separates “something accepts and then hangs up” from a service that is only intermittently listening.

After the fix

  • Re-run the HTTP check and confirm the URL returns a real status code from outside your network.
  • Test every hostname that points at the server (apex, www, any subdomain), since a default-server rule only catches the names you forgot.
  • If the cause was a crash or a timeout, watch for it under real traffic for a day. Intermittent empty responses come back when the load does.

Check your own domain now

Free, no sign-up. Runs the exact check this guide describes and shows what to fix.