Proxy error · ECONNRESET

ECONNRESET and socket hang up

ECONNRESET means the other end closed the connection hard while your client still expected to use it. Node reports it as read ECONNRESET, or as socket hang up when it happens before any response arrived. Through a proxy the questions are which hop hung up, and whether your client was reusing a connection the other side had already given up on.
Whose fault is it?

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
With a proxy in the path

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.

The key question

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.reusedSocket is true on the failed request in Node’s http module: 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.
Cheapest first

Fixes, in the order to try them

  1. Retry idempotent requests once

    A GET that failed with ECONNRESET can 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
  2. 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’s http module, a lower agent timeout, or keepAlive: false if you would rather open a fresh connection every time.

    Costs nothing
  3. 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
  4. 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 nothing
  5. 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.

    Costs some time
Per tool

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
terminal
# 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
reset.py
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 + spider
# 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
reset.py
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
reset.mjs
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.

Before you buy anything

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.

The meter

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.

From the metering section of our terms of service.

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.

ECONNRESET, asked often

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.

The community layer

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 Discord

4,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.

Join the Discord4,200+ monkeys, free to lurk