Skip to content
Latchkey LogoLatchkey home

RPC failed with curl 56 in GitHub Actions

RPC failed with curl 56 in GitHub Actions means the clone started and the connection died while the pack was still arriving. curl 56 is the generic receive failure, so the words after it name the TLS library rather than the cause, and the cause is nearly always something between the runner and the remote rather than the repository.

Runner log: a clone reset mid-pack with curl 56, then the retry cloning 593 commits
The recorded run: a proxy resets the connection part way through the pack, the runner retries the clone, and the second attempt completes.
Diagram of a pack transfer cut mid-stream, the fixes that work, and the runner action
The transfer is a download, which is why the buffer setting everyone recommends is for the other direction.

What this error means

The checkout reaches "Receiving objects", gets some way through, and stops. The first line names the transport, "error: RPC failed; curl 56", followed by whatever the TLS library called it. The lines under it are the consequences: a count of bytes still expected, a fetch-pack disconnect while reading the sideband, an early EOF, and the remote end hung up unexpectedly. Small repositories clone fine and the large one fails, often at a different percentage each run, which is the clearest sign that nothing is wrong with the repository. curl exit 56 is documented as a failure with receiving network data, and the sentence after it changes with the build rather than with the cause, which is why two logs of the same failure can read very differently.

Actions log, checkout step
error: RPC failed; curl 56 GnuTLS recv error (-9): Error decoding the received TLS packet.
error: 4096 bytes of body are still expected
fetch-pack: unexpected disconnect while reading sideband packet
fatal: early EOF
fatal: fetch-pack: invalid index-pack output

Reproduced on a Latchkey runner

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

attempt 1, git version 2.55.0, proxy cuts the stream: yes
error: RPC failed; curl 56 GnuTLS recv error (-9): Error decoding the received TLS packet.
error: 4096 bytes of body are still expected
fetch-pack: unexpected disconnect while reading sideband packet
fatal: early EOF
fatal: fetch-pack: invalid index-pack output
proxy: reset the connection after 1500000 bytes
[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, git version 2.55.0, proxy cuts the stream: no
cloned 593 commits

Retried the git fetch/clone with exponential backoff after a transient transport error

Which curl number you have narrows it a lot

Git reports the curl exit code straight through, and the codes are documented and distinct. Reading yours takes a second and rules out most of the page.

The words after the number are the TLS library talking, not a second diagnosis. On the Ubuntu runner image git links against GnuTLS and reports a TLS packet it could not decode; an OpenSSL build of the same git calls the same event a connection reset by peer. Search on the number.

Codecurl's definitionWhat it usually is in CI
6Could not resolve hostDNS, not the transfer
18A transfer was shorter or larger than expectedA proxy truncating a long response
56Failure with receiving network dataThe connection dropped mid-pack
92Stream error in the HTTP/2 framing layerAn intermediary mishandling HTTP/2
22The requested URL returned an errorA push rejected by a body size limit

Common causes

Something between the runner and the remote dropped the connection

A proxy, an egress gateway, a NAT table that expired an entry, or a load balancer recycling a backend. The transfer is long and the path has several hops that can each end it. This is the ordinary case and the one a retry fixes, which is why the failing percentage moves between runs.

You raised http.postBuffer and nothing changed

The wasted fix, and it is nearly universal on this error because the advice is everywhere. The buffer governs request bodies, so it changes pushes and leaves fetches alone. In our experience a team that has raised it to 500 MB and still sees curl 56 on clones has spent a week on the wrong setting, and the giveaway is in the log: a clone is receiving objects, not sending them.

The repository is large enough that one blip is enough

A multi-gigabyte history takes long enough that the chance of a single interruption approaches certain on a busy network. Nothing is broken; the transfer is simply a big target. Large binary files and long histories both contribute, and a shallow clone removes most of both.

HTTP/2 streams are being mishandled on the way

Some proxies and gateways do not cleanly carry a long-lived HTTP/2 stream and reset it. That usually shows as curl 92 rather than 56, sometimes as both across runs on the same network. Forcing HTTP/1.1 sidesteps the whole path, at a small cost in connection overhead.

How to fix it

Fetch only what the job needs

  1. Leave actions/checkout at its default depth of 1 unless something genuinely needs history.
  2. When a tool needs tags or a version count, fetch those specifically after the checkout.
  3. For a large repository that really does need history, use a partial clone so blobs arrive on demand.
.github/workflows/ci.yml
- uses: actions/checkout@v7
  with:
    fetch-depth: 1
    submodules: false
- name: Only if a tool needs it
  run: git fetch --depth=50 origin "$GITHUB_REF_NAME"

Retry the clone, with a ceiling

A dropped transfer is the definition of a retryable failure, so wrap the clone rather than re-running the job by hand. Keep the ceiling low: three attempts is enough for a transient drop, and anything that fails three times is not transient.

Terminal
for attempt in 1 2 3; do
  git clone --depth 1 "$REPO" work && break
  echo "clone attempt $attempt failed, retrying"
  rm -rf work; sleep 5
done

Force HTTP/1.1 when the code is 92

When the message names an HTTP/2 stream, pinning the protocol avoids the path that is failing. It is a per-machine setting, so it belongs in a setup step rather than in each command.

Terminal
git config --global http.version HTTP/1.1
git clone --depth 1 https://github.com/org/big-repo.git

Change the path when the network keeps cutting it

If the same clone fails repeatedly from one network and never from another, the answer is the network. SSH takes a different route through most proxies and is not subject to HTTP body limits. A local mirror of a large third-party repository removes the long transfer entirely.

Terminal
git clone git@github.com:org/big-repo.git
# or clone once into a cache the fleet shares
git clone --mirror https://github.com/org/big-repo.git /srv/mirrors/big-repo.git

Fetch less, and the failure stops being likely

The probability of a drop scales with how long the transfer is open, so the most reliable fix is to move less data. Most CI jobs need one commit on one branch and take the whole history by habit.

actions/checkout already defaults to a depth of 1. If your workflow sets fetch-depth: 0 for a tool that wants tags or history, scope it: fetch the tags you need afterwards rather than the whole graph, or use a partial clone that fetches blobs on demand.

.github/workflows/ci.yml
- uses: actions/checkout@v7
  with:
    fetch-depth: 1

# when a tool needs tags, but not every object
- run: git fetch --depth=1 --tags --no-recurse-submodules

# when it needs history, but not every blob
- run: git clone --filter=blob:none --single-branch "$REPO" work

http.postBuffer is for pushes, and this is a fetch

Raising http.postBuffer is the most widely copied answer to this error and it is the wrong direction. The setting controls how large a request body git will buffer before it switches to chunked transfer encoding, so it affects what you send. A clone receives, and the buffer is not in the path.

It does help on the other failure with a similar look: a push rejected with HTTP 400 and curl 22 because a proxy refused a chunked body. If your log is a push, raise it. If your log is a clone, spend the time on the depth instead.

Terminal
# for a push refused with curl 22 and HTTP 400
git config --global http.postBuffer 524288000

# for a clone that dies with curl 56, this does nothing

What the runner does about it

Latchkey detects this as GIT_FETCH_TRANSIENT at confidence 0.88, and its plan is a retry with backoff, three attempts starting at 3 seconds. One pattern covers the whole family on purpose: the library's own description lists early EOF, RPC failed, a remote that hung up and an index-pack failure together, on the grounds that they are the same transport event seen from different places in git.

It is also careful about what it will not touch. Its recorded false-positive note says it matches git-specific transient transport phrases only and will not match a repository that does not exist or an authentication failure, which is the right line: retrying a 404 or a bad token is how a clear error becomes a slow one. On the recorded run the proxy in front of the clone reset the connection after 1.5 MB, the runner retried, and the second attempt cloned 593 commits.

How to prevent it

  • Keep checkout shallow by default, and justify every fetch-depth: 0.
  • Retry clones in a bounded loop instead of re-running the workflow by hand.
  • Record the curl code on failure, because it is the difference between four causes.
  • Mirror large third-party repositories the fleet clones repeatedly.

Frequently asked questions

What does curl error 56 mean?
curl documents it as a failure with receiving network data: the transfer was underway and the connection stopped delivering. The text after the number comes from the TLS library, so the same event reads as a decode error on a GnuTLS build and as a connection reset on an OpenSSL build. The number is the part to search for.
Does increasing http.postBuffer fix a failed clone?
No. It sets how much of a request body git buffers before switching to chunked encoding, so it affects pushes. A clone is a download and never touches it. Raising it is harmless and it is also why so many teams spend days on this error without moving.
Why does a large repository clone locally but fail in CI?
Because the path is different. A runner sits behind shared egress, often behind a proxy, and the transfer competes with every other job on the machine. The repository is the same; the number of hops that can drop a long connection is not. Shallow cloning removes most of the exposure.
How do I retry a failed git clone in GitHub Actions?
Wrap the clone in a bounded loop, or put the retry on the step that fails rather than on the workflow. Delete the partial directory between attempts, because git will not clone into a non-empty path and the second attempt then fails for a new reason that hides the first.

Related guides

References

curl 56 is the connection dropping, not the repository. Latchkey retries the fetch mid-job with backoff. Start free → 30-day trial · No credit card