Troubleshooting

ERR_CONNECTION_RESET: find who sent the reset and fix it

ERR_CONNECTION_RESET means a TCP reset killed the connection after it opened. If every site fails, check VPN, proxy, antivirus; if one, its server or firewall.

Published ~10 min read

Short answer: ERR_CONNECTION_RESET means the connection to the site was opened and then killed by a TCP reset (RST) packet, sent by the server or by something between you and it. If every site fails, the cause is on your side: a VPN, proxy, antivirus or network filter. If one site fails from every network, its server or a firewall in front of it cuts the connection.

One request from a terminal shows what the browser hides:

Linux · macOS
curl -sSv -o /dev/null https://example.com/
Windows PowerShell
curl.exe -sSv -o NUL https://example.com/

Replace example.com with the failing site. If the request lines (> GET) are followed by Recv failure: Connection reset by peer (Windows: Connection was reset), the server or a device in front of it accepted the request and reset it. The same line right after Client hello points at TLS, cause 5. If curl works while the browser is reset, look at your own machine: a proxy setting, an extension, or antivirus that only filters browsers.

Which message do you see?

WhereMessageWhat happened
Chrome, EdgeThis site can’t be reached · The connection was reset. · ERR_CONNECTION_RESETAn RST arrived after the connection was open (Chromium net error -101)
FirefoxThe connection was resetThe same event
curl, HTTPcurl: (56) Recv failure: Connection reset by peerReset after the request was sent
curl 7.88, HTTPScurl: (35) Recv failure: Connection reset by peerReset during the TLS handshake
PythonConnectionResetError: [Errno 104] Connection reset by peerThe same; 104 is ECONNRESET on Linux
Node.jsError: read ECONNRESETThe same. socket hang up means the server closed cleanly without answering
nginx error.logrecv() failed (104: Connection reset by peer) while reading response header from upstreamYour app reset nginx; the visitor gets a 502, not a reset
sshkex_exchange_identification: read: Connection reset by peerThe SSH variant, with its own causes (fail2ban, MaxStartups)

The Chrome, curl, Python and Node lines are real output (Chrome 153, Debian 12). Three neighbours get mixed up with it. ERR_CONNECTION_REFUSED is the answer to the very first packet: a reset because nothing listens, or a firewall’s reject. ERR_CONNECTION_TIMED_OUT means no answer at all. ERR_EMPTY_RESPONSE (curl: Empty reply from server) means the server closed the connection cleanly without sending anything.

For the SSH message, read SSH connection reset by peer instead.

Is it every site, or only one?

PatternThe reset comes fromGo to
Every site, on one deviceThat device: VPN, proxy, antivirus, broken network stackYour computer
Every site, every device on one networkThe router, the ISP or a network filterRestart the router; test over mobile data
One site, from every network and deviceThe server or a firewall in front of itSite owners
One site, only from one networkSomething on that path: an ISP or office filter, MTUYour computer, MTU

Fixes on your own computer

  • Open the site in a private window. That switches off extensions, including proxy and VPN add-ons. Clearing cache and cookies rarely helps: a reset happens below HTTP, where neither is involved.
  • Turn off the VPN and any proxy. On Windows the proxy is under Settings → Network & internet → Proxy, on a Mac under System Settings → Network → your connection → Details → Proxies.
  • Pause antivirus web protection. Products that scan HTTPS open every connection themselves and reset what they cannot parse. If the site then loads, add an exception and switch the shield back on.
  • Try another network, such as your phone’s hotspot. If that works, your ISP, school or office filter resets the connection, usually after reading the hostname from the TLS handshake (SNI).
  • An old firewall and a new browser. Since Chrome and Edge 124 (April 2024) the TLS ClientHello carries a post-quantum key share and no longer fits into one packet. Firewalls and TLS-inspecting proxies with outdated firmware reset such connections. Chrome’s temporary policy to switch it off ended with Chrome 146, so the fix is a firmware update of that device.

If every site fails on one Windows PC, reset its network stack in an administrator terminal, then restart:

Windows · administrator terminal
netsh winsock reset
netsh int ip reset
ipconfig /flushdns

To test the MTU, send a full 1500-byte packet that may not be fragmented (1472 bytes of data plus 28 of headers). If 1472 fails while 1400 gets replies, a link on the way carries less. Many hosts drop pings entirely, so replace TARGET with an address that answers small ones:

Windows
ping -f -l 1472 TARGET
ping -f -l 1400 TARGET
Linux
ping -c 3 -M do -s 1472 TARGET
ping -c 3 -M do -s 1400 TARGET

If only your connection is affected (a VPN, PPPoE or a mobile router), find the largest size that still gets replies, add 28, and set that as the adapter’s MTU. Site owners fix it for everyone with MSS clamping.

Windows · administrator terminal
netsh interface ipv4 show subinterfaces
netsh interface ipv4 set subinterface "INTERFACE" mtu=NEW_MTU store=persistent

Site owners: find out who sends the reset

Reproduce the reset with curl while the server captures resets on port 443. VISITOR_IP is the address curl runs from; drop the host part to see all clients:

Debian · Ubuntu · on the server
sudo tcpdump -ni any 'tcp port 443 and host VISITOR_IP and tcp[tcpflags] & tcp-rst != 0'

Without the flag filter you see the whole conversation; here a test server reads the request, then resets it:

Example output · tcpdump, timestamps and options trimmed
IP 127.0.0.1.34914 > 127.0.0.1.47101: Flags [S], seq 788105413, win 65495, length 0
IP 127.0.0.1.47101 > 127.0.0.1.34914: Flags [S.], seq 3469401065, ack 788105414, win 65483, length 0
IP 127.0.0.1.34914 > 127.0.0.1.47101: Flags [.], ack 1, win 512, length 0
IP 127.0.0.1.34914 > 127.0.0.1.47101: Flags [P.], seq 1:80, ack 1, win 512, length 79
IP 127.0.0.1.47101 > 127.0.0.1.34914: Flags [.], ack 80, win 511, length 0
IP 127.0.0.1.47101 > 127.0.0.1.34914: Flags [R.], seq 1, ack 80, win 512, length 0
  • Flags [R.] leaving your server: your server sent it. Causes 1 to 4 below.
  • The client is reset, but the server capture shows no RST: a device in between forged it, such as a filter on the visitor’s network, an IPS, or a DDoS filter in front of your server. In a capture on the client (Wireshark, or tcpdump -v), compare the ttl of the RST with that of your server’s SYN-ACK: a forged reset started at another hop and usually differs.

Our HTTP header checker requests the URL from Amsterdam. If it gets a normal answer while one visitor keeps getting resets, the reset is specific to that visitor: their path, or a rule matching their address (cause 3).

Why visitors rarely see your app’s reset

A browser only sees resets from the machine it talks to; anything further back turns into an HTTP error. A browser reset on a site behind nginx therefore came from nginx or from something in front of it:

SetupWhen the app resets the connectionThe visitor sees
App on a public port (Node, Gunicorn, a Go binary)The browser gets the RSTERR_CONNECTION_RESET
nginx, Apache or Caddy in frontThe proxy logs recv() failed (104: …) or upstream prematurely closed connection502 Bad Gateway
Cloudflare proxy in frontCloudflare gets the RST or the 502Error 520 (or the 502)

Cause 1: the process holding the connection dies or stops reading

When a process exits, the kernel closes its sockets, and a socket with unread request data is closed with a reset. That covers an app killed by the OOM killer, a crash, a restart during a deploy, and an app server that rejects an upload without reading it. Check first:

Debian · Ubuntu
# killed by the OOM killer, or crashed?
sudo journalctl -k --since "1 hour ago" | grep -iE 'killed process|segfault'

# restarts and errors of the app's own service
systemctl status APP_SERVICE --no-pager
sudo journalctl -u APP_SERVICE --since "1 hour ago" --no-pager | tail -n 50

If only large uploads are reset, a size limit refuses the upload and closes while the browser is still sending. Tomcat reads at most 2 MiB of a rejected upload (maxSwallowSize); beyond that, its docs say the client will hardly see the response. nginx, with lingering_close on by default, reads and discards the rest for up to 30 seconds, so the browser gets the 413. Put the limit into nginx (client_max_body_size).

With Docker, curl on the host can print the reset when the app in the container listens only on 127.0.0.1: docker-proxy accepts and has nowhere to forward. Make the app listen on 0.0.0.0 inside the container. From outside, the same mistake usually shows as connection refused.

Cause 2: nginx resets connections on purpose

return 444 closes a connection without a response. By default that is a clean close and the browser shows ERR_EMPTY_RESPONSE. With reset_timedout_connection on, nginx sends an RST instead, for 444 and for requests that hit client_body_timeout or send_timeout (60 seconds without progress by default; idle keep-alive connections still close cleanly). A slow upload or a stalled download on a weak mobile link then ends in ERR_CONNECTION_RESET. Find the directives in the loaded configuration:

Debian · Ubuntu
sudo nginx -T 2>/dev/null | grep -nE 'return 444|reset_timedout_connection|lingering_close|client_max_body_size'

A 444 in a catch-all default_server for unknown hostnames is fine; one that real visitors hit is your reset. Never set lingering_close off, which nginx’s documentation says breaks the protocol.

Cause 3: firewall rules, fail2ban and rate limits

What the visitor sees depends on when the rule matches:

  • Rejecting new connections produces refused, not reset. ufw reject turns into --reject-with tcp-reset for TCP, but ufw lets established traffic through before your rules, so only the first packet is answered.
  • fail2ban’s stock iptables action rejects with icmp-port-unreachable (the nftables action with reject), and its chain sits at the top of INPUT, so a ban also hits open connections: the page stalls, new connections are refused. A reset only appears if someone set blocktype to REJECT --reject-with tcp-reset.
  • Rules that match open connections are the real source: connection or rate limits written with --reject-with tcp-reset, an IPS or WAF in front of the server, a DDoS filter upstream. nginx’s limit_req and limit_conn answer with an HTTP status (503 by default), not a reset.
Debian · Ubuntu · read-only checks, then one unban
# Is this visitor banned, and in which jail?
sudo fail2ban-client banned VISITOR_IP

# Rules that send resets
sudo iptables -S | grep -i reset
sudo nft list ruleset | grep -iE 'reset|reject'

# Lift a ban
sudo fail2ban-client unban VISITOR_IP

The visitor can read their address on our what is my IP page.

Cause 4: the kernel runs out of socket memory

Under heavy load the kernel resets connections it can no longer afford: orphaned sockets beyond tcp_max_orphans, or TCP memory above tcp_mem. It logs that, and its counters show which reset is rising:

Debian · Ubuntu
sudo dmesg -T | grep -E 'TCP: (too many orphaned sockets|out of memory)'

# run twice, a minute apart: the second run shows only what changed
nstat TcpOutRsts TcpExtTCPAbortOnClose TcpExtTCPAbortOnData TcpExtTCPAbortOnMemory TcpExtListenOverflows
  • TcpExtTCPAbortOnClose: an app closed a socket with unread data, cause 1.
  • TcpExtTCPAbortOnData: a deliberate reset with SO_LINGER 0, such as nginx’s 444 with reset_timedout_connection.
  • TcpExtTCPAbortOnMemory: this cause. Find what holds so many connections before raising limits.
  • TcpOutRsts also counts answers to scanners on closed ports, so it always grows. TcpExtListenOverflows (a full accept queue) drops connections instead of resetting them, unless net.ipv4.tcp_abort_on_overflow is 1; the default is 0.

Cause 5: resets during the TLS handshake

Real curl output, shortened, from a server that resets as soon as the ClientHello arrives:

Example output · curl -sSv -o /dev/null https://…
* Connected to 127.0.0.1 (127.0.0.1) port 47101 (#0)
* ALPN: offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* Recv failure: Connection reset by peer
* OpenSSL SSL_connect: Connection reset by peer in connection to 127.0.0.1:47101
curl: (35) Recv failure: Connection reset by peer

nginx and Apache answer TLS problems with an alert, which browsers report as ERR_SSL_PROTOCOL_ERROR or a similar code, not with a reset. A reset here comes from something else on the port (a TCP load balancer, a port forward, a VPN sharing 443) or from a device that reads the ClientHello: an SNI filter, a TLS-inspecting firewall, or one that chokes on the large post-quantum ClientHello. curl on Debian 12 (OpenSSL 3.0) sends a small ClientHello and can pass where Chrome is reset; OpenSSL 3.5 on Debian 13 offers the X25519MLKEM768 key share by default, so compare both:

Debian 13 · OpenSSL 3.5
openssl s_client -connect example.com:443 -servername example.com -groups X25519MLKEM768 -brief </dev/null
openssl s_client -connect example.com:443 -servername example.com -groups X25519 -brief </dev/null

If the first is reset and the second connects, a device on the path cannot handle large ClientHellos. Update or replace it; no setting on the web server fixes it.

Cause 6: MTU and MSS on tunnels

Behind a tunnel, for example a VPS that forwards ports to a home server over WireGuard, full 1500-byte packets no longer fit. If the ICMP “fragmentation needed” messages are filtered, large packets vanish: the connection opens, small requests work, and it hangs at the first full-size packet, ending in a timeout or, when a device gives up, a reset. If nginx on the VPS proxies to the home server, the kernel already sizes those connections for the tunnel. Traffic the VPS forwards (DNAT) needs MSS clamping, which rewrites the segment size in the handshake to fit:

iptables · e.g. in the tunnel's PostUp
iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
nftables · inside the forward chain
tcp flags syn tcp option maxseg size set rt mtu

Behind Cloudflare

With the orange cloud, the browser’s TCP connection ends at Cloudflare, and whatever your origin does arrives as a Cloudflare error page: a reset after connecting becomes a 520, a refused connection a 521, no answer a 522. So ERR_CONNECTION_RESET on a proxied hostname happens between the visitor and Cloudflare: their network, their antivirus, or a filter that blocks Cloudflare addresses or that hostname. Check that the record really is proxied: dig +short example.com must return Cloudflare addresses (AS13335).

Our ASN lookup tells you which network an address belongs to.

If it returns your server’s own IP, the record is DNS-only and causes 1 to 6 apply. On a VMHeaven VPS in Amsterdam or Frankfurt you have root for tcpdump, nstat and the firewall, and the VNC console in the panel still reaches the server when a rule locks out SSH.

Frequently asked

Is ERR_CONNECTION_RESET my fault or the website's?

Test another site and another network. If every site fails on your device, the cause is local: a VPN, proxy, antivirus or a broken network stack. If only one site fails, from mobile data and other devices too, its server or a firewall in front of it sends the reset, and only the site owner can fix it.

What is the difference between ERR_CONNECTION_RESET and ERR_CONNECTION_REFUSED?

They happen at different moments. Refused is the answer to the very first packet: a TCP reset because nothing listens on the port, or a firewall's reject. Reset means the connection was already open, often with the request sent, when the server or a device in between killed it with a reset.

Can a firewall cause ERR_CONNECTION_RESET?

Yes, when it acts on connections that are already open: a rule that rejects with a TCP reset, an IPS, a TLS-inspecting firewall or antivirus, or an outdated firewall that cannot handle the larger post-quantum TLS handshake of current browsers. Rules that only reject new connections cause refused instead.

Related articles

All troubleshooting