ECONNRESET and socket hang up
Usually timing, not blame: a connection one side had already closed. Resets on fresh connections point at the exit or the site.
- Usually from
- Idle pooled connections; sometimes the exit or the site
- Try first
- Retry idempotent requests once on a fresh connection
What a reset connection means through a proxy
Node’s documentation describes ECONNRESET as a connection “forcibly closed by a peer”, normally after a timeout or reboot on the remote side. socket hang up is Node’s own message, carrying the same ECONNRESET code, for a connection that closed before any response arrived. One that closes partway through a response gives aborted instead.
Keep-alive is the classic cause. Your client parks a finished connection to reuse it. The proxy or the site closes its end after its own idle timeout. The next request goes out on the dead connection and gets reset. Node’s docs describe exactly this race and suggest retrying when req.reusedSocket is true, and the global HTTP agent has kept connections alive by default since Node 19.
Through a rotating residential gateway there is a second cause: the home connection your tunnel leaves through can drop mid-request, and a retry leaves through a different exit. undici’s fetch reports either case as TypeError: fetch failed, with ECONNRESET or UND_ERR_SOCKET (“other side closed”) on err.cause.
Your credentials, the proxy, or the target?
Usually timing, not blame: a connection one side had already closed. Resets on fresh connections point at the exit or the site.
Your credentials
Not the cause. A wrong login gets a 407, not a hang-up.
The proxy
The gateway, like any server, closes idle connections after a while, and a residential exit can drop mid-request. Both look like resets from your side.
The target site
Sites close idle keep-alive connections too, and some reset connections they do not want. Resets on one site only, on fresh connections, point there.
How to tell
req.reusedSocketis true on the failed request in Node’shttpmodule: it went out on a pooled connection that had gone stale.- Resets follow a pause in traffic and vanish under steady load: idle timeouts, not blocks.
- curl reports the same thing as
Recv failure: Connection reset by peer, and Python Requests as('Connection aborted.', ConnectionResetError(104, 'Connection reset by peer')). - Resets on one host only, even on brand-new connections, point at that host or at how it treats the exit.
Fixes, in the order to try them
- Costs nothing
Retry idempotent requests once
A GET that failed with
ECONNRESETcan safely go again, ideally on a new connection. Do not blindly repeat a POST that may already have been processed, and keep retries to one or two with a short, growing pause. - Costs nothing
Let your client drop idle connections first
Keep the client’s idle limit below the server’s, so your side closes a parked connection before the other side does. In undici that is
keepAliveTimeout; with Node’shttpmodule, a lower agenttimeout, orkeepAlive: falseif you would rather open a fresh connection every time. - Costs nothing
Use a new agent when you rotate
A tunnelled connection stays on the exit it was opened through. After a reset, or whenever you want a different exit, close the old agent and create a new one instead of reusing its sockets.
- Costs nothing
Put a deadline on every request
A request that sits for minutes on a slow exit invites a reset from somebody. A connect timeout and an overall deadline turn a hang into a clean error you can retry.
- Costs some time
Log whether the socket was reused
Record
reusedSocket, the host and the time since the previous request. If resets only hit reused sockets, it is keep-alive timing; if they hit fresh ones on one host, look at that host.
See the real status and headers
Each sample retries a reset once on a fresh connection, and keeps pooled connections from outliving the other side’s patience.
curl
# one retry after a reset, on a new connection
curl -s -o /dev/null -x http://USER:[email protected]:8000 --retry 1 --retry-all-errors --retry-delay 2 \
-w '%{http_code} after %{num_connects} new connection(s)\n' https://example.com/
# curl: (56) Recv failure: Connection reset by peer--retry-all-errors repeats any failed request, so keep it to GETs and other requests that are safe to send twice.
Python Requests
import requests
from requests.adapters import HTTPAdapter
from urllib3.util import Retry
PROXY = "http://USER:[email protected]:8000"
retry = Retry(total=2, connect=1, read=1, backoff_factor=1, allowed_methods={"GET", "HEAD"})
with requests.Session() as http:
http.mount("https://", HTTPAdapter(max_retries=retry))
http.mount("http://", HTTPAdapter(max_retries=retry))
try:
r = http.get("https://example.com/", proxies={"http": PROXY, "https": PROXY}, timeout=(10, 30))
print(r.status_code)
except requests.exceptions.ConnectionError as err:
print(err)
# ('Connection aborted.', ConnectionResetError(104, 'Connection reset by peer'))allowed_methods keeps the retry to requests that are safe to repeat.
Scrapy
# settings.py: dropped connections are on Scrapy's default retry list
RETRY_TIMES = 2
DOWNLOAD_TIMEOUT = 60
# in the spider
from scrapy.exceptions import DownloadFailedError, ResponseDataLossError
from twisted.internet.error import ConnectionLost
async def start(self):
for url in self.start_urls:
yield scrapy.Request(url, errback=self.on_error)
def on_error(self, failure):
if failure.check(ConnectionLost, DownloadFailedError, ResponseDataLossError):
self.logger.warning("connection dropped after retries: %s", failure.request.url)Playwright
import time
from playwright.sync_api import Error, sync_playwright
PROXY = {"server": "http://resi.proxymonkey.io:8000", "username": "USER", "password": "PASS"}
def load(page, url, attempts=2):
for attempt in range(1, attempts + 1):
try:
return page.goto(url, wait_until="domcontentloaded")
except Error as err:
dropped = "ERR_CONNECTION_RESET" in err.message or "ERR_EMPTY_RESPONSE" in err.message
if not dropped or attempt == attempts:
raise
time.sleep(2 * attempt)
with sync_playwright() as p:
browser = p.chromium.launch(proxy=PROXY)
page = browser.new_page()
print(load(page, "https://example.com/").status)
browser.close()Chromium calls a reset net::ERR_CONNECTION_RESET, and a connection closed before any reply net::ERR_EMPTY_RESPONSE.
Node.js
import { fetch, ProxyAgent } from "undici";
const newDispatcher = () => new ProxyAgent({ uri: "http://USER:[email protected]:8000", keepAliveTimeout: 2_000 });
let dispatcher = newDispatcher();
const DROPPED = new Set(["ECONNRESET", "UND_ERR_SOCKET"]);
const codeOf = (err) => {
for (let e = err; e; e = e.cause) if (typeof e.code === "string") return e.code;
return "";
};
async function get(url, attempts = 2) {
for (let i = 1; ; i++) {
try {
return await fetch(url, { dispatcher, signal: AbortSignal.timeout(30_000) });
} catch (err) {
if (!DROPPED.has(codeOf(err)) || i === attempts) throw err;
await dispatcher.close();
dispatcher = newDispatcher();
await new Promise((resolve) => setTimeout(resolve, 1_000 * i));
}
}
}
console.log((await get("https://example.com/")).status);This retries GETs only. undici’s RetryAgent can do the same, and its default retry list includes ECONNRESET and UND_ERR_SOCKET for methods that are safe to repeat.
The samples use the residential gateway, resi.proxymonkey.io:8000. For an ISP or datacenter IP, use USER:PASS@IP:PORT for the address you rented. Your dashboard lists the host and port for every order, and where it differs from this page, the dashboard is right.
Will a different proxy line fix it?
When switching helps
When the resets come from residential exits dropping during long transfers, a static ISP or datacenter IP keeps a connection up for longer, if the target accepts that kind of address.
When it will not
Keep-alive timing is a client setting. A stale pooled connection gets reset on any line, so fix the agent first.
Is a failed request billed?
We bill for request bytes and response bytes, including headers and protocol overhead on the tunnelled connection. Connections that fail before transferring data are not billed. Retries that you initiate are billed and appear as separate entries in your usage log.
The terms bill request and response bytes, and bill retries you initiate as separate entries. A reset that cuts a response short has usually moved some bytes already, and the terms do not describe a partial response on its own. A reset before anything transferred is a connection that failed before transferring data, which is not billed.
Residential is billed per GB of that traffic. ISP and datacenter addresses are charged per IP for their term, and where a plan includes a traffic allowance, traffic past it is billed per GB under the same rule. The usage log in your dashboard has one row per request with bytes in, bytes out and cost, so you can look up the failed request yourself.
Questions people ask about a reset connection
What is the difference between ECONNRESET and socket hang up?
They carry the same code. Node says socket hang up when the connection closed before any response arrived, and aborted when it closed partway through one. A bare read ECONNRESET comes from the socket itself.
Is it safe to retry after ECONNRESET?
For GET, HEAD and other requests that do the same thing twice, yes, once or twice with a pause. A POST may have reached the server before the reset, so check before repeating it or you may submit twice.
Why does it happen after my script sits idle?
The pooled connection outlived the other side’s idle timeout. Your client only finds out when it writes the next request into a connection the proxy or the site already closed. Lower the client’s keep-alive timeout, or retry requests that failed on a reused socket.
Does ECONNRESET mean I have been blocked?
Usually not. Resets after idle periods are timing. Resets on one site only, every time, on fresh connections, can be that site turning the traffic away; slow down and check you are welcome there.
Errors that travel with this one
- TIMEOUT
Connection timeout
Nothing came back in time. Find out which leg stalled before you raise the limit.
Work it out → - 502
502 Bad Gateway
A go-between got a bad answer from the next hop. Could be the proxy, could be the site’s own load balancer.
Work it out → - ProxyError
Requests ProxyError
Python could not get through the proxy. The outer message never changes; the reason is in the innermost brackets.
Work it out →
Still stuck on a reset connection?
Paste your error in the Discord: the full message plus the command or the few lines that set up the proxy, with the password taken out. Someone there has seen it before.
Join the Discord4,200+monkeys in the Discord
Help from humans
Post your error, get an answer. Usually in minutes, usually from someone who has hit the same wall.
A status bot that tells on us
Pool health, incidents and maintenance posted automatically. Including the bad days.
Deals and free traffic
Bonus GB drops, early access to new pools, and the occasional giveaway for a good bug report.