Skip to content
Latchkey LogoLatchkey home

ECONNRESET and connection reset by peer in CI

ECONNRESET in GitHub Actions means a connection that was already open was torn down by a reset packet, which is a different failure from a timeout: something answered you, and then something ended the conversation. Every tool in the job has its own sentence for that one event, so the useful question is not which tool printed it but where in the path the reset came from.

Runner log: curl exit 56 and a Node socket hang up from one reset, then both transfers completing
The recorded run: one server, one reset, two different sentences. The retried attempt moved all 8,388,608 bytes and the Node request got its response.
Diagram of refused, timed out and reset connections and which fix each one needs
Three failures people call the same thing. A reset is the only one where the connection was established first, which is what makes a retry worth trying.

What this error means

A step that downloads, installs, clones or calls an API dies part way through, and the same job passes when you re-run it. The wording depends on who was holding the socket. curl reports exit 56 and a receive failure. npm prints its own code line. Node reports either a read error or a socket hang up, and attaches code: 'ECONNRESET' to both. Go programs, the Docker client and the kubelet among them, print the raw syscall with both endpoints in it. None of them says which hop sent the reset, and that is what decides whether a retry is the fix or a waste of two minutes. The run below produces the event on purpose. A TLS server on loopback closes with a zero linger timeout, so the close sends a reset rather than a graceful shutdown, and two clients hit it at different moments: curl part way through an eight megabyte body, and Node before any response byte was written. Everything above the wire is real, including both clients' TLS stacks and the messages they print. The server cuts only the first attempt, because a peer that resets forever models a dead host rather than the blip this page is about.

Actions log, download step
curl: (56) Recv failure: Connection reset by peer
Error: socket hang up
    at TLSSocket.socketOnEnd (node:_http_client:528:25)
  code: 'ECONNRESET'

Reproduced on a Latchkey runner

Run 2026-09-20·Runner latchkey-small·Exit code 0

attempt 1, curl 8.5.0 (x86_64-pc-linux-gnu) libcurl/8.5.0, node v20.20.2, server cuts the stream: yes
curl: (56) Recv failure: Connection reset by peer
Error: socket hang up
    at TLSSocket.socketOnEnd (node:_http_client:528:25)
  code: 'ECONNRESET'
server: reset the download after 1048576 of 8388608 bytes
server: reset GET /api/session HTTP/1.1 before writing a response byte
curl exited 56, node exited 1
[latchkey-bash-wrapper] BEGIN sidecar POST (boot_wait=30s max_time=320s url=http://localhost/diagnose socket=/run/latchkey-self-heal/sock)
[latchkey-bash-wrapper] END sidecar POST ok (attempts=1 http=200)
attempt 2, curl 8.5.0 (x86_64-pc-linux-gnu) libcurl/8.5.0, node v20.20.2, server cuts the stream: no
curl: got 8388608 bytes, download complete
node: read 4096 bytes, request complete

The same event, in seven different logs

Every line below is one TCP reset. If you are searching for the exact string in your log, start here, then read this page once rather than seven pages once each.

What the log saysWho printed itWhen the reset landed
curl: (56) Recv failure: Connection reset by peercurl, and anything shelling out to itWhile reading the response body
npm error code ECONNRESET, or npm ERR! code ECONNRESET before npm 10npm, mid-installWhile fetching a package or a manifest
error: RPC failed; curl 56git over HTTPSWhile receiving the pack
read tcp 10.0.0.5:51234->44.205.64.79:443: read: connection reset by peerGo tools: the Docker client, the kubelet, k6On a socket read, with both endpoints and their ports
Error: socket hang up with code: ECONNRESETNode, via http or httpsBefore the response headers arrived
Error: read ECONNRESET at TLSWrap.onStreamReadNode, same codeAfter headers, during the body
curl: (56) OpenSSL SSL_read: Connection reset by peer, errno 104curl built against OpenSSLThe same moment as the first row

Common causes

Something in the middle tore the connection down

A proxy with an idle timeout, a NAT table dropping a long-lived entry, a load balancer recycling the backend, or an appliance that decided your transfer had gone on long enough. None of these are visible from the runner, and all produce a reset rather than a clean close. This is the ordinary case, and the one a retry fixes.

The peer was overloaded, restarting, or finished with you

A registry node under load, a service rolling out a new version, or a server whose keep-alive window closed a moment before your next request went down the same socket. That race is why this shows up as a flaky test far more often than as a flaky download.

Your job opened more connections than the path allows

Parallel installs, a matrix of jobs behind one egress address, and a load test all push the same lever. Hosted runners share their outbound address, so the number of connections a remote sees from what looks like one client is larger than anything your workflow declared. In our experience resets that cluster in a highly parallel job are the easiest to fix, because turning the concurrency down is one line.

The test shut down the server it was talking to

The flaky-test version, and the one where retries are the wrong answer. A test that tears down its server while a client is still connecting, or reuses a keep-alive socket across a teardown, produces a reset whenever the timing goes the wrong way. The error is real; the cause is in the test.

How to fix it

Retry with the client that owns the transfer

  1. Use the tool's own retry, not a retry around the step, so the backoff and the resume happen where the state is.
  2. For curl, --retry with --retry-all-errors, because a reset mid-body is not in curl's default retryable set.
  3. For package managers, raise their fetch retry counts rather than re-running the whole install.
Terminal
curl --retry 5 --retry-delay 2 --retry-all-errors --fail --show-error \
  --location --output artifact.tgz "$URL"

Open fewer sockets at once

Lower the number of connections your job makes against one host. Every package manager has a knob for it, and a matrix has max-parallel. This is the fix that turns a reproducible cluster of resets into nothing at all, for a little wall clock.

Terminal
npm config set maxsockets 8
pip install --no-input -r requirements.txt   # pip is serial by default
export COMPOSER_MAX_PARALLEL_HTTP=6

Take the transfer off the critical path

A reset can only break a download that happens. Cache the dependency store, pull images through a mirror, and keep large artifacts inside your own network. A package restored from a cache is not a connection anything can reset.

.github/workflows/ci.yml
- uses: actions/setup-node@v7
  with:
    node-version: 22
    cache: npm

For tests, stop the reset instead of retrying it

Keep the server alive for the whole test, handle errors on the server side rather than letting an upgrade handler throw, and do not reuse keep-alive sockets across a teardown. If a single transient reset during setup is genuinely expected, retry the connect a bounded number of times and only for that code.

test.mjs
for (let i = 0; i < 3; i++) {
  try {
    await connect(url);
    break;
  } catch (e) {
    if (e.code !== 'ECONNRESET') throw e;
  }
}

Refused, timed out and reset are three different answers

Node documents ECONNRESET as "A connection was forcibly closed by a peer. This normally results from a loss of the connection on the remote socket due to a timeout or reboot." The important word is forcibly: a reset is an action taken by something, not the absence of an answer.

The words after curl's number are its TLS library talking, not a second diagnosis. The curl on the Ubuntu runner image calls a mid-body reset a receive failure; an OpenSSL build calls the identical event "OpenSSL SSL_read: Connection reset by peer, errno 104". Search on the 56.

ECONNREFUSED means nothing was listening, and no retry inside the same minute will change that. ETIMEDOUT means packets went out and nothing came back, which usually points at a firewall that drops rather than refuses, or at a host that is gone. ECONNRESET means the connection was established, work was underway, and something ended it: the peer, or a box in the middle. That is the only one of the three where retrying the identical request has a good chance of working, which is why sorting your failure into the right bucket before you touch anything is worth the minute it takes.

Terminal
# curl tells you which one it had, in its exit code
# 7 refused, 28 timed out, 56 reset while receiving
curl --silent --show-error --fail https://registry.example.com/health
echo "curl exit $?"

What happened on the recorded run

The first attempt lost both transfers to one reset and the second completed both, with no change to the command. The runner is a Latchkey managed runner, and what sits between those attempts is its own retry of the failed step. No pattern is named here: naming one asserts a specific production detector fired, and that belongs to a page whose evidence record carries it.

The shape is worth keeping whatever you run on. A reset that clears on the next attempt was a blip; one that repeats with the same host and request is policy: a proxy rule, a size limit, a TLS inspection box. The first is worth three retries, the second a packet capture. See how self-healing works for what a managed runner does with a step that failed this way.

How to prevent it

  • Give every network step in CI a bounded retry that belongs to the tool doing the work.
  • Cap install and download concurrency so one job is not fifty connections to one host.
  • Cache or mirror the transfers that repeat on every run.
  • Keep real-network tests separate from unit tests, so one reset fails one suite.

Frequently asked questions

What causes ECONNRESET in GitHub Actions?
A reset packet from the peer or from something between you and it: a proxy idle timeout, a load balancer recycling a backend, a NAT table eviction or a server under load. It is not caused by your dependency tree or your lockfile, which is why the same commit passes on a re-run. The one exception is a test that shuts down its own server, where the reset is real and the cause is local.
Is socket hang up the same as ECONNRESET?
In Node, yes. A socket hang up is what the HTTP client prints when the connection closed before a response arrived, and the error object it throws carries code: ECONNRESET. A read ECONNRESET is the same event landing later, after headers, while the body was still being read. Both point at the same wire event and take the same fixes.
What does curl exit code 56 mean?
curl documents 56 as CURLE_RECV_ERROR, "Failure with receiving network data". On a reset it prints "Recv failure: Connection reset by peer" beside it. Note that exit 56 is about receiving, so it tells you the connection had been established and data was already moving, which rules out DNS and refused connections.
Should I retry a step that fails with connection reset by peer?
Retry it once or twice, and watch what happens. A reset caused by a blip in the path clears on the next attempt; one caused by policy, a size limit or a middlebox rule reproduces exactly, with the same host and the same byte count. The second kind is worth investigating rather than retrying, because no number of attempts will change it.

Related guides

References

A TCP reset is not something you can catch in review. Latchkey runners repair the transient failures. Start free → 30-day trial · No credit card