# RPC failed with curl 56 in GitHub Actions

> Fix RPC failed curl 56 in GitHub Actions: the clone died mid-transfer, so fetch less, retry the drop, and skip the buffer setting that does nothing.

Source: https://latchkey.dev/learn/failures/git-rpc-failed-curl-56-in-ci  
Updated: 2026-09-20

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.

## 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
```

## 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
```

## 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.

## 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.

| Code | curl's definition | What it usually is in CI |
| --- | --- | --- |
| 6 | Could not resolve host | DNS, not the transfer |
| 18 | A transfer was shorter or larger than expected | A proxy truncating a long response |
| 56 | Failure with receiving network data | The connection dropped mid-pack |
| 92 | Stream error in the HTTP/2 framing layer | An intermediary mishandling HTTP/2 |
| 22 | The requested URL returned an error | A push rejected by a body size limit |

> Code 6 belongs to [EAI_AGAIN and could not resolve host in GitHub Actions](/learn/failures/eai-again-could-not-resolve-host-in-ci) rather than here, and code 22 on a push is a body limit rather than a dropped transfer.

## 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.

## FAQ

### 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.

## References

- [libcurl error codes 6, 18, 22, 56 and 92](https://curl.se/libcurl/c/libcurl-errors.html)
- [git config: http.postBuffer and http.version](https://git-scm.com/docs/git-config)
- [git clone: --depth, --single-branch and --filter](https://git-scm.com/docs/git-clone)
- [actions/checkout: fetch-depth](https://github.com/actions/checkout)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
