# Connection Reset by Peer (ECONNRESET): Causes and Fixes

> "Connection reset by peer" means the other side sent a TCP RST. When it arrives tells you who sent it: idle pool, restart, proxy or limit. Fixes for each.

1. [Home](/)
2. [Blog](/blog)
3. Connection Reset by Peer: Who Sent the RST, and When

DeploymentOctober 31, 202610 min read

# Connection Reset by Peer: Who Sent the RST, and When

"Connection reset by peer" and ECONNRESET mean the other side of the socket sent a TCP reset. The moment it arrives (on connect, after idle, mid-response, mid-upload) tells you who sent it and what to fix.

## The short version

"Connection reset by peer" (ECONNRESET, errno 104) means the machine on the other end of the socket, or a proxy in between, aborted the connection with a TCP RST instead of closing it normally. When it happens points to the sender: on the first request after a pause it's a pooled connection the server already closed; mid-response it's a restart or crash, which sometimes shows as an unexpected EOF instead; on connect it's a service refusing you before the protocol starts.

[Teo Marquardt](/about#author)· Facts last checked October 29, 2026

The usual pattern: it fails once an hour, maybe twice a day. Nothing in your code changed, the database is up, and retrying the same request works. The log says:

text

```
Error: read ECONNRESET
    at TCP.onStreamRead (node:internal/stream_base_commons:217:20) {
  errno: -104,
  code: 'ECONNRESET',
  syscall: 'read'
}
```

The stack trace is no help: it ends inside Node's socket code, not yours. What the message does tell you is that you didn't close this connection. Something on the other side did, abruptly, while your process still thought the connection was usable. Finding the bug means working out who that was, and the most useful clue is when the reset arrived. There are four moments a reset can arrive, and each one points at a different culprit.

## What "connection reset by peer" means

A TCP connection normally ends with a FIN: one side says it has nothing more to send, the other side finishes reading and closes too. A reset is different. An RST segment means "this connection no longer exists here, drop it now". Anything still in flight is thrown away, and the next `read()` or `write()` on your socket fails with `ECONNRESET`. The [Node.js documentation](https://nodejs.org/api/errors.html#common-system-errors) describes it as a connection forcibly closed by a peer, usually after a timeout or reboot on the remote side.

"Peer" means whatever answers at the far end of your socket, which is not always the server you meant to talk to. A load balancer, a NAT gateway, a firewall or a Docker port proxy can each send the RST on the server's behalf. A TCP stack sends one in a few situations ([RFC 9293](https://www.rfc-editor.org/rfc/rfc9293) covers the rules): a packet arrives for a connection it has no record of, or an application closes a socket while unread data is still sitting in its receive buffer. Firewalls, NAT gateways and load balancers can also inject an RST into connections they track. Each of those maps to a cause below.

Don't confuse it with its neighbour. With `ECONNREFUSED` the connection never opened, because nothing was listening on the port; that one is covered in [ECONNREFUSED 127.0.0.1](/blog/econnrefused-127-0-0-1). A reset means the connection was open, or at least accepted, and then was torn down.

When matters more than the message

The text of the error is the same whoever sent the RST. The timing is not. Note whether it fails on connect, on the first request after a quiet period, halfway through a response, or while uploading. That one observation narrows the cause more than any amount of reading the stack trace.

## One reset, many error messages

Every runtime reports the same RST in its own words, which is why people searching for it rarely find each other's answers. These all mean the same thing:

| Where           | What you see                                                                                      | Code                                                                                                 |
| --------------- | ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| Node.js         | Error: read ECONNRESET, or Error: socket hang up with code: 'ECONNRESET' when no response arrived | errno -104 on Linux                                                                                  |
| Python          | ConnectionResetError: \[Errno 104\] Connection reset by peer                                      | 104                                                                                                  |
| Python requests | ConnectionError: ('Connection aborted.', ConnectionResetError(104, 'Connection reset by peer'))   | 104                                                                                                  |
| Go              | read tcp 10.0.0.5:51234->10.0.0.9:5432: read: connection reset by peer                            | syscall.ECONNRESET                                                                                   |
| Java            | java.net.SocketException: Connection reset                                                        | n/a                                                                                                  |
| curl            | curl: (56) Recv failure: Connection reset by peer                                                 | exit code 56                                                                                         |
| Windows         | WSAECONNRESET, "An existing connection was forcibly closed by the remote host"                    | 10054                                                                                                |
| macOS / BSD     | ECONNRESET                                                                                        | 54                                                                                                   |
| nginx error.log | recv() failed (104: Connection reset by peer)                                                     | 104\. Your app reset nginx's connection: see [502 Bad Gateway in nginx](/blog/502-bad-gateway-nginx) |

Database drivers often add their own wording on top. node-postgres reports a connection that disappeared under it as `Error: Connection terminated unexpectedly`, and MySQL clients report a lost server connection. Underneath, the cause is usually one of the resets below. The same RST also shows up in game clients, SAP GUI and package managers behind corporate proxies; the mechanism is the same, the causes are their own.

### ConnectionResetError: \[Errno 104\] Connection reset by peer

In Python it usually surfaces from `requests`, `urllib3`, a database driver or `asyncio`, with the real exception buried a few frames down:

text

```
Traceback (most recent call last):
  ...
  File "/usr/lib/python3.12/ssl.py", line 1103, in read
    return self._sslobj.read(len, buffer)
ConnectionResetError: [Errno 104] Connection reset by peer

During handling of the above exception, another exception occurred:
  ...
requests.exceptions.ConnectionError: ('Connection aborted.', ConnectionResetError(104, 'Connection reset by peer'))
```

Its sibling is `BrokenPipeError: [Errno 32] Broken pipe`, or `EPIPE` in Node. That's the same broken connection, noticed while writing instead of reading. Treat both the same way.

## When the reset arrives tells you who sent it

If you read one thing on this page, make it this table. Find the row that matches when your error happens:

| When it fails                                         | Who most likely sent the RST                                                        | Where to look                                                       |
| ----------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| First request after a few seconds or minutes of quiet | The server, a proxy or a NAT gateway closed an idle connection your pool still held | Idle timeouts on the pool versus the server and anything in between |
| In the middle of a response, often during a deploy    | The process on the other side crashed, restarted or was stopped                     | Deploy times, restart counts, SIGTERM handling                      |
| Immediately on connect, every time                    | A service refusing you before the protocol starts                                   | Bind address, TLS versus plain, sshd limits, firewall               |
| While uploading a large body                          | The server rejected the request and closed without reading the rest                 | Body size limits on the server or proxy                             |

Intermittent resets in production are almost always the first two rows. The third is usually a setup problem that fails every time, and the fourth shows up with large uploads only.

To see who sent it, capture resets on the client host while you reproduce the error: `tcpdump -ni any 'tcp[tcpflags] & tcp-rst != 0'`. The source address of the RST is the sender. If it's a proxy or gateway address rather than the server's, the server may never have closed anything.

## Reset on the first request after idle

This is the classic `read ECONNRESET` after a few minutes idle. Your app keeps connections open in a pool so it doesn't pay for a new TCP and TLS handshake on every request. HTTP agents, database drivers and Redis clients all work this way. While a connection sits unused, something else may decide it has been idle too long and close it: the server itself, a load balancer, or a NAT gateway that drops its record of the connection after a few minutes. Your pool isn't told. The next query is written into a connection that no longer exists on the other side, and the reply is an RST.

For scale, as of writing: a Node.js server closes idle keep-alive connections after 5 seconds, MySQL closes idle sessions after 28800 seconds, and Linux sends its first TCP keepalive probe after 7200 seconds. Anything in between may have its own limit, often a few minutes. That explains the usual symptoms. It fails on the first request after a quiet period, works on retry because the retry opens a fresh connection, and shows up at night or in staging, where traffic is sparse. Under steady load the pool never sits still long enough.

The rule that fixes it: **the side that holds the connection idle must close it first.** Your pool's idle timeout has to be shorter than the shortest idle timeout on the server and on every proxy or gateway between you. Waiting for the operating system to notice doesn't work either: Linux's `net.ipv4.tcp_keepalive_time` defaults to 7200 seconds, two hours, far longer than most NAT and load balancer idle limits.

### HTTP clients

Between two HTTP services the race is tight. A Node.js server closes idle keep-alive connections after 5 seconds by default (`server.keepAliveTimeout`). On recent Node versions the client reuses connections by default too: `http.globalAgent` has kept connections alive since Node 19, and `fetch` pools them as well. If the client reuses a connection just as the server's timer fires, the request goes out on a closed socket and you get `socket hang up`. Set the server timeout comfortably longer than the client's, and if you control the client, give its agent a shorter idle timeout explicitly:

javascript

```
// Server: keep idle connections longer than any client keeps them
const server = app.listen(process.env.PORT);
server.keepAliveTimeout = 65_000;
server.headersTimeout = 66_000; // keep it above keepAliveTimeout

// Client: close idle sockets well before the server would
import { Agent } from "node:http";
const agent = new Agent({ keepAlive: true, timeout: 4_000 });
```

That agent setting covers `node:http` and `node:https` clients. `fetch` in Node runs on undici, which has its own `keepAliveTimeout` (4 seconds by default as of writing) and follows the server's `Keep-Alive` hint when one is sent.

The same logic applies to a reverse proxy in front of your app: if the proxy keeps upstream connections open longer than your app does, the app resets the proxy. That case is covered from nginx's side in the [502 article](/blog/502-bad-gateway-nginx).

### Database and Redis pools

A self-managed PostgreSQL doesn't close idle sessions by default (`idle_session_timeout` is 0), and MySQL waits eight hours (`wait_timeout` is 28800 seconds). Managed services and poolers may set shorter limits. So when a database connection resets after a few minutes idle, look first at what sits between you and the database: a pooler, a proxy or a NAT gateway. Three settings handle it, and most drivers have all three:

* Close idle connections early. node-postgres closes a pooled client after `idleTimeoutMillis`, which defaults to 10 seconds as of writing. mysql2 pools have `idleTimeout`, 60 seconds by default. SQLAlchemy has `pool_recycle`, which replaces connections older than the given number of seconds.
* Check connections before use. SQLAlchemy's `pool_pre_ping=True` runs a cheap query when a connection leaves the pool and replaces it if that fails. Some pools call this validation or a test-on-borrow setting.
* Turn on TCP keepalive in the client: node-postgres takes `keepAlive: true`, mysql2 has `enableKeepAlive`. Small keepalive packets stop a NAT gateway from forgetting the connection, and reveal a dead one sooner.

javascript

```
import pg from "pg";

const pool = new pg.Pool({
  connectionString: process.env.DATABASE_URL,
  idleTimeoutMillis: 10_000, // the default; keep it below every idle limit in between
  keepAlive: true,
});

// An idle client can die in the pool. Without this listener,
// that error crashes the process.
pool.on("error", (err) => console.error("idle client error", err));
```

The `pool.on("error")` line matters. node-postgres emits an error when an idle client in the pool loses its connection, and an unhandled `error` event takes a Node process down, which turns one harmless reset into a restart. Many Redis clients reconnect automatically, but a command that was in flight when the connection dropped may fail, depending on the client and its settings. If you run a pooler such as PgBouncer between the app and Postgres, the same rule applies to the hop from the app to the pooler, which is covered in [PgBouncer and connection pooling](/blog/pgbouncer-connection-pooling).

## Reset in the middle of a response

A reset partway through a response means the process on the other side went away while it was working: it crashed, it was restarted, or it was stopped during a deploy. When a process exits, the kernel closes its sockets. A socket closed with unread data in its buffer sends an RST; one without sends a FIN, which clients report as `socket hang up`, `Connection terminated unexpectedly` or an unexpected EOF. For debugging, treat them as the same event.

If the resets line up with deploys, the old version is being stopped with requests still in flight. Either the app ignores SIGTERM and gets killed when the grace period ends (that's [exit code 137](/blog/exit-code-137)), or it exits on SIGTERM straight away without finishing what it was doing. The fix is a shutdown that stops accepting new work, lets current requests finish, then closes the pool:

javascript

```
process.on("SIGTERM", () => {
  server.close(async () => {
    await pool.end();
    process.exit(0);
  });
  server.closeIdleConnections?.(); // Node 18.2+
  setTimeout(() => process.exit(1), 8_000).unref();
});
```

The same applies when the restart happens on the far side. A database restart, failover or version upgrade drops every open connection, so each pooled client gets a reset the next time it's used. With a short idle timeout and a pre-use check, the pool discards those connections instead of handing them to your code. For how rolling and blue-green releases overlap old and new instances, see [blue-green vs rolling deployments](/blog/blue-green-vs-rolling-deployments).

## Reset on connect

If it fails right away on every attempt, something accepted the TCP connection and then refused to deal with you. This is a configuration problem, and retrying won't help.

### curl: (56) Recv failure: Connection reset by peer from Docker

text

```
$ docker run -p 8080:3000 my-app
$ curl localhost:8080
curl: (56) Recv failure: Connection reset by peer
```

The port is published and something answers on 8080, yet every request resets. The app inside the container is listening on `127.0.0.1`, which there means the container's own loopback. Docker forwards the connection to the container's network address, and nothing listens there. Depending on how Docker forwards the port, you see a reset (curl 56) or `curl: (52) Empty reply from server`. Either way the fix is in the app: listen on `0.0.0.0`.

javascript

```
// Reachable only from inside the container
app.listen(3000, "127.0.0.1");

// Reachable through the published port
app.listen(3000, "0.0.0.0");
```

Dev servers are the usual offenders, because many bind to localhost by default: Vite needs `--host`, Flask needs `--host=0.0.0.0`, and Rails needs `-b 0.0.0.0`.

### kex\_exchange\_identification: read: Connection reset by peer

This one comes from SSH, and from `git` over SSH. The connection was accepted, then closed before the two sides even exchanged version strings. The usual causes: sshd is refusing unauthenticated connections because too many are pending (`MaxStartups`, default `10:30:100`: once 10 are pending it drops 30% of new connections, rising to all of them at 100); fail2ban or a firewall has blocked your IP after earlier failures; `hosts.deny` rejects you; or whatever listens on that port isn't sshd at all. Check the server's auth log and the ban list first.

A third variant is a protocol mismatch: a client speaking plain TCP or plain HTTP to a port that expects TLS, or the reverse. Many servers reset rather than answer garbage. If a database or Redis connection resets on connect, check whether the server requires TLS and your connection string asks for it.

## Reset while you are still sending

Large uploads produce a subtler version. The server reads the headers, sees a body over its limit, sends an error and closes the socket. But the client is still sending the rest of the body. Data arriving at a closed socket gets an RST, and the client sees `ECONNRESET` or `EPIPE` instead of the error response the server meant to send. nginx avoids this with lingering close (on by default, for up to 30 seconds), reading and discarding the rest of the body before it closes, but plenty of app servers and proxies don't. If small requests work and big ones reset, treat it as a size limit; [413 Request Entity Too Large](/blog/413-request-entity-too-large) covers where those limits live.

## Retry, but only what is safe to repeat

A retry fixes most resets, because the second attempt gets a fresh connection. But retrying everything is how a payment gets charged twice. The question is whether the server might already have acted on the request before the connection died.

* Safe to retry: GET, HEAD, OPTIONS, PUT and DELETE, which are idempotent by definition ([RFC 9110, section 9.2.2](https://www.rfc-editor.org/rfc/rfc9110#section-9.2.2)). A request that failed on a reused pooled connection before any response came back is usually the idle reset from above, where the server never saw it. Usually, not always: the server may have received it and then crashed, so extend this only to reads or to writes that carry an idempotency key.
* Not safe to retry blindly: POST and other writes that failed mid-response. The server may have finished the work and died before replying. Retry these only with an idempotency key the server uses to recognise the repeat.
* Retry once, on a new connection. If the second attempt resets too, it's not a stale connection, and more retries only hide the problem.

javascript

```
const IDEMPOTENT = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
// fetch in Node reports a server-side close as UND_ERR_SOCKET
const RESET_CODES = new Set(["ECONNRESET", "EPIPE", "UND_ERR_SOCKET"]);

async function requestWithRetry(url, options = {}) {
  const method = (options.method ?? "GET").toUpperCase();
  try {
    return await fetch(url, options);
  } catch (error) {
    const code = error.cause?.code ?? error.code;
    if (!IDEMPOTENT.has(method) || !RESET_CODES.has(code)) throw error;
    return fetch(url, options); // one retry; string or buffer bodies only
  }
}
```

Database drivers have the same issue in a different form. Re-running a `SELECT` after a reset is fine. Re-running an `INSERT` outside a transaction may insert twice, so either make the write idempotent (a unique key, `ON CONFLICT DO NOTHING`) or check whether it landed before repeating it.

## How Runsite handles it

On Runsite, deploys roll: new containers start next to the old ones, traffic moves only after the new version passes its health checks, and a release that fails them is rolled back automatically. [Runsite web services give you zero-downtime rolling deploys on European infrastructure](/services/web-services), but the old containers still get stopped at the end, so the SIGTERM handler above is what keeps their last requests from being reset. A container that crashes is restarted on a healthy node. Point the health check at an endpoint that touches the database, not one that only proves the port is open, so a release that can't reach its data never takes traffic.

Your services reach [managed PostgreSQL over private networking](/services/postgresql), with TLS by default, so the connection string you copy is already the right one for TLS. A failed instance of [managed Redis restarts on a healthy node](/services/redis); connections drop while that happens, so retry the first call after a reconnect. Pool idle timeouts and the retry rules above stay in your code on any platform. The services and databases run in Germany.

## The short version

* "Connection reset by peer" and ECONNRESET mean the far side of the socket, or a proxy in between, aborted the connection with an RST.
* Note when it fails. First request after idle: a stale pooled connection. Mid-response: a restart or crash. On connect: a bind address, TLS or sshd problem. During uploads: a size limit.
* For idle resets, make the client pool's idle timeout shorter than every idle timeout downstream, enable TCP keepalive, and check connections before use.
* For deploy resets, handle SIGTERM: stop accepting work, finish what's in flight, close the pool.
* Retry once on a fresh connection, and only requests that are idempotent or never reached the server.

Related service

## Web Services on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore Web Services](/services/web-services)

[Back to all articles](/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### What does "connection reset by peer" mean?

It means the other end of a TCP connection aborted it with a reset (RST) packet instead of closing it normally with a FIN. Any data in flight is discarded, and the next read or write on your side fails with ECONNRESET, errno 104 on Linux. The peer is whatever answers at the far end of the socket: the server, or a load balancer, NAT gateway, firewall or proxy in between. Common causes are a pooled connection that was closed while idle, a server process that restarted or crashed, and a server that refused the connection or rejected an oversized request.

### How do I fix a connection reset error?

Look at when it happens. If it fails on the first request after a quiet period, your connection pool reused a connection the server or a proxy had already closed: make the pool's idle timeout shorter than the server's, enable TCP keepalive, and validate connections before use. If it fails mid-response during deploys, make the server handle SIGTERM and finish in-flight requests before exiting. If it fails on every connect, check the bind address (0.0.0.0 in Docker), TLS settings and firewall rules. Then retry once, only for idempotent requests.

### What is "kex\_exchange\_identification: read: Connection reset by peer"?

It is an SSH error meaning the server accepted the TCP connection and closed it before the key exchange began. Common causes are sshd's MaxStartups limit dropping new unauthenticated connections when too many are pending, fail2ban or a firewall blocking your IP after failed attempts, a hosts.deny rule, or a service other than sshd listening on that port. Check the server's auth log and ban list. The same error appears when git connects over SSH.

### What is ECONNRESET?

ECONNRESET is the operating system error code for a connection that was forcibly closed by the remote side. Node.js reports it as Error: read ECONNRESET, or as socket hang up when no response arrived; Python raises ConnectionResetError with errno 104; Windows uses WSAECONNRESET, error 10054\. It differs from ECONNREFUSED, where the connection was never accepted because nothing listened on the port. With ECONNRESET the connection existed and was then torn down.

Keep reading

## Related articles

[Deployment9 min readECONNREFUSED 127.0.0.1: Your App Is Calling ItselfError: connect ECONNREFUSED 127.0.0.1:6379 after a deploy means your app is calling its own machine. Why the address says localhost, the ::1 variant, and how to fix each cause for Redis and Postgres.Oct 7, 2026Read](/blog/econnrefused-127-0-0-1)[Deployment9 min read502 Bad Gateway in Nginx After Deploy: Read the Error LogA 502 Bad Gateway page from nginx says nothing about the cause. The matching line in error.log does: connection refused, a worker that died mid-response, a keep-alive reset or an oversized header. What each one means and how to fix it after a deploy.Oct 9, 2026Read](/blog/502-bad-gateway-nginx)[Deployment10 min readExit Code 137: SIGKILL, Not Always Out of MemoryExit code 137 says a process was killed with SIGKILL. It doesn't say who sent it. How to tell a memory kill from a missed SIGTERM on deploy or a failing probe, and what fixes each one.Oct 5, 2026Read](/blog/exit-code-137)

## Your app deserves to be online

€5 of credit on signup. Deploy in under a minute. No credit card needed.

[Start deploying](https://dashboard.runsite.app/login)[View documentation](https://docs.runsite.app)

---

Source: https://runsite.app/blog/connection-reset-by-peer
