GitLab CI Error Guide: 'The remote end hung up unexpectedly' — Fix Git Clone Failures in Jobs
Fix 'fatal: the remote end hung up unexpectedly' in GitLab CI: tune shallow clone depth, HTTP post buffer, timeouts, and Git strategy so large-repo and LFS clones stop dropping mid-fetch.
- #gitlab
- #ci-cd
- #troubleshooting
- #errors
Stuck on this GitLab CI/CD error? Get the free incident triage checklist
A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.
Overview
Every GitLab CI job begins with the runner cloning or fetching your repository into the build directory. When that Git transfer is interrupted before it completes, the job fails in the “Getting source from Git repository” section — before your script: ever runs — with:
fatal: the remote end hung up unexpectedly
fatal: early EOF
fatal: index-pack failed
Over HTTP you’ll often see the companion curl/RPC lines just above it:
error: RPC failed; curl 92 HTTP/2 stream 5 was not closed cleanly
error: RPC failed; curl 18 transfer closed with outstanding read data remaining
fatal: expected flush after ref listing
The transfer started, then the connection was cut mid-stream. This is almost always about transfer size, memory, timeouts, or a proxy — not a corrupt repo.
Symptoms
- The job fails in the “Getting source from Git repository” step; your scripts never execute.
- Failures are intermittent — the same pipeline sometimes passes, especially on smaller MRs.
- Large repositories, repos with big binaries, or Git LFS objects fail more often.
- Failures correlate with a
git fetch/git cloneof a deep history or a runner behind a proxy/VPN. - Retrying the job sometimes succeeds, suggesting a flaky or throttled network path.
Common Root Causes
- Repository or single objects too large — deep history or large binaries blow past HTTP buffer/timeout limits mid-transfer.
GIT_STRATEGY: cloneon a huge repo — a full clone every job moves far more data than a shallow fetch.- Missing or too-deep
GIT_DEPTH— no shallow clone means the runner pulls all history; too shallow can also break when a needed ref isn’t present. - HTTP post buffer / server limits — the default
http.postBufferis small; large packs get chunked in ways a strict proxy or gateway drops. - Proxy, load balancer, or gateway timeouts — an idle/duration cap (e.g. 60s) on a reverse proxy in front of GitLab cuts long transfers.
- Runner memory pressure —
git index-packruns out of memory unpacking a large pack and the process dies (early EOF). - Git LFS interruptions — large LFS objects time out or hit smudge/bandwidth limits during checkout.
- HTTP/2 quirks — some proxies mishandle HTTP/2 streams, producing
curl 92 ... not closed cleanly.
Diagnostic Workflow
1. Confirm where it fails and how big the repo is. The error in “Getting source” (not in script:) points at the fetch. Measure the repo:
# Repo size and largest objects (run in the repo, or on the runner host)
du -sh .git
git count-objects -vH
git rev-list --objects --all |
git cat-file --batch-check='%(objecttype) %(objectsize) %(rest)' |
sort -k2 -n | tail -20
2. Switch to a shallow fetch and set a sane depth. For most CI you only need the tip commit(s). Set the Git strategy and depth in .gitlab-ci.yml (project or job level):
variables:
GIT_STRATEGY: fetch # reuse the cached working copy instead of full clone
GIT_DEPTH: "20" # shallow history; enough for most diffs/tags
GIT_SUBMODULE_STRATEGY: none
Set GIT_DEPTH: "0" only when a job genuinely needs full history (e.g. git describe across many tags) — and pair it with the buffer/timeout tuning below.
3. Raise the HTTP post buffer and disable HTTP/2 if the errors are curl 92/18. Do this in a pre_get_sources_script (runs before the clone) or in the runner’s config:
default:
hooks:
pre_get_sources_script:
- git config --global http.postBuffer 524288000 # 500 MB
- git config --global http.version HTTP/1.1 # sidestep flaky HTTP/2 proxies
- git config --global http.lowSpeedLimit 1000 # bytes/sec
- git config --global http.lowSpeedTime 300 # allow slow transfers up to 5 min
4. Prefer the runner’s clone over HTTP proxies where possible. On the runner config (config.toml), reasonable clone settings and enough disk/memory matter:
[[runners]]
# give large-repo jobs a generous per-job timeout at the runner level
[runners.custom_build_dir]
enabled = true
[runners.cache]
# cache clones between jobs to reduce transfer volume
If a reverse proxy fronts GitLab, raise its client/idle timeouts (e.g. Nginx proxy_read_timeout, client_max_body_size, and send_timeout) so long fetches aren’t cut.
5. For LFS failures, verify and stage LFS separately. Check whether LFS objects are the failing transfer:
git lfs env
git lfs ls-files | wc -l
GIT_TRACE=1 GIT_CURL_VERBOSE=1 git lfs pull # verbose to see where it drops
Consider GIT_LFS_SKIP_SMUDGE: "1" during clone and pulling LFS explicitly with retries in a script step.
6. Rule out memory. If you see fatal: early EOF / index-pack failed, index-pack may be OOM-killed. Check the runner:
dmesg -T | grep -i 'killed process' # OOM killer evidence
free -h
Give the runner (or the job’s container) more memory, or reduce pack size via shallow fetch.
Example Root Cause Analysis
A backend repo with several years of history and a few large seed-data binaries started failing roughly one job in four with RPC failed; curl 92 HTTP/2 stream ... not closed cleanly followed by fatal: the remote end hung up unexpectedly. Small doc-only MRs always passed.
Diagnosis:
- The failure was always in “Getting source from Git repository,” never in scripts — so it was the fetch, not the build.
du -sh .gitreported 3.1 GB, and the largest-objects listing showed several 200 MB+ binaries committed years earlier still weighting every clone.- The pipeline used the default
GIT_STRATEGY: clonewith noGIT_DEPTH, so every job re-cloned full history through a corporate reverse proxy that terminated HTTP/2 streams past ~60s.
Fix, applied in stages:
variables:
GIT_STRATEGY: fetch
GIT_DEPTH: "20"
default:
hooks:
pre_get_sources_script:
- git config --global http.version HTTP/1.1
- git config --global http.postBuffer 524288000
Switching to a shallow fetch cut the per-job transfer from ~3 GB to under 50 MB, and forcing HTTP/1.1 sidestepped the proxy’s HTTP/2 stream handling. The intermittent failures stopped. As a follow-up the team scheduled a history rewrite to move the large binaries to LFS, permanently shrinking the pack.
Prevention Best Practices
- Default to
GIT_STRATEGY: fetchwith a smallGIT_DEPTH(10–50); use full depth only for the specific jobs that need it. - Keep large binaries out of Git history — use Git LFS or an artifact registry — so every clone isn’t dragging dead weight.
- Set
http.postBufferandhttp.version HTTP/1.1viapre_get_sources_scripton repos that transfer large packs behind strict proxies. - Raise reverse-proxy/load-balancer read and idle timeouts in front of GitLab so long transfers aren’t severed.
- Ensure runners have enough memory for
index-packon your largest repo; watch for OOM kills. - Use
GIT_LFS_SKIP_SMUDGEplus an explicit, retriedgit lfs pullstep for LFS-heavy repos. - Add job-level
retry:for the transient network class so a one-off drop self-heals:retry: { max: 2, when: [runner_system_failure, stuck_or_timeout_failure] }.
Quick Command Reference
# Measure repo weight and find the largest objects
du -sh .git && git count-objects -vH
git rev-list --objects --all | git cat-file --batch-check='%(objectsize) %(rest)' | sort -n | tail -20
# Tuning applied before the clone (pre_get_sources_script)
git config --global http.postBuffer 524288000
git config --global http.version HTTP/1.1
git config --global http.lowSpeedLimit 1000
git config --global http.lowSpeedTime 300
# LFS diagnostics
git lfs env && git lfs ls-files | wc -l
GIT_TRACE=1 GIT_CURL_VERBOSE=1 git lfs pull
# Look for OOM-killed index-pack on the runner host
dmesg -T | grep -i 'killed process'
free -h
# Shallow, resilient source-fetch defaults in .gitlab-ci.yml
variables:
GIT_STRATEGY: fetch
GIT_DEPTH: "20"
build:
script: ["make build"]
retry:
max: 2
when: [runner_system_failure, stuck_or_timeout_failure]
Conclusion
fatal: the remote end hung up unexpectedly in a GitLab job is a source-fetch failure, not a build bug: the Git transfer was cut mid-stream by size, buffer, timeout, memory, or proxy limits. The highest-leverage fixes are switching to a shallow GIT_STRATEGY: fetch with a modest GIT_DEPTH, raising http.postBuffer (and forcing HTTP/1.1 on flaky proxies), and keeping large binaries out of Git history via LFS. Add a targeted retry: for transient network drops, and clones become fast and reliable even on large repositories.
Fixed it? Get 500 GitLab CI/CD & DevOps AI prompts — free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.
Did this fix your issue?
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.