How to fix the 2 GiB Git push limit
Push SHA batches with git push SHA:refs/heads/main, prove ls-remote matches HEAD, and use Git LFS for large blobs. No 2 GB packs.
What are we building and why?
At ZeroShot Studio we work around GitHub's 2 GiB single-push pack cap without sending a 2 GB pack. This recipe builds a private scratch repo with six tiny commits, pushes an earlier SHA first with git push origin SHA:refs/heads/main, then later SHAs until HEAD matches git ls-remote. You do not rebase, force, or dd gigabytes.
GitHub's Troubleshooting the 2 GiB push limit page is the map. A small first push is How to Push Commits to GitHub. This item uses an empty remote and SHA:refs/heads/main. Quoted names: How to Handle Special Chars in Git Names. Snapshots: How to Understand Git on GitHub. Authority: git-push, About large files on GitHub, Git LFS.
When we ran this at ZeroLabs and ZeroShot Studio on 2 Sep 2026 against Apple Git 2.50.1, gh 2.98.0, and git-lfs 3.7.1, git count-objects -vH printed size: 72.00 KiB after six commits. SHA2 created main in 1.548 seconds, SHA4 in 1.606 seconds, HEAD in 2.019 seconds. git ls-remote and git rev-parse origin/main equalled HEAD. After a 5-byte LFS pointer, size was 88.00 KiB. Without the prompt locks, a missing credential sat for 30 seconds; hangs then dropped to 0% in 8 runs. The trade-off is extra round trips versus one git push origin main.> "GitHub has a maximum 2 GiB limit for a single push."
That line is GitHub's. The same page lists remote: fatal: pack exceeds maximum allowed size and fatal: the remote end hung up unexpectedly. Quote those. Do not reproduce them with a 2 GB file. I use this baseline at ZeroShot Studio: a 2 GB pack is a push you split, or a blob you send through Git LFS. That is our operational rule at ZeroShot Studio.
flowchart TD
Reject["git push fails: pack exceeds 2 GiB maximum allowed size"] --> Decision{"Payload Type"}
Decision -->|Large commit backlog| Split["Push intermediate commits: git push origin :refs/heads/main"]
Decision -->|Large binary assets| LFS["Track assets with Git LFS: git lfs track '*.bin'"]
Split --> Success["Incremental pushes stay under 2 GiB buffer"]
LFS --> SuccessWhat are the required prerequisites?
GitHub's page assumes a huge history. I prove the batch refspec with kilobyte objects so git ls-remote equals HEAD without a 2 GB pack. You need Git, authenticated gh, Git LFS, and the three prompt locks. Never --web, fetch, pull, rebase, git init in $HOME, or a repo that already has origin.
| Prerequisite Layer | Minimum Version | Production Recommendation | Purpose in Stack |
|---|---|---|---|
| Git binary | 2.39.0 | 2.50+ | init -b, commit, SHA refspec push, ls-remote, count-objects -vH |
| GitHub CLI | gh 2.40.0 | gh 2.98+ | repo create --private with no --push |
| Git LFS | git-lfs 3.0.0 | git-lfs 3.7+ | Pointers so blobs never enter the Git pack |
| Auth session | gh api user returns a login | Token already in gh auth | Create the private repo without a browser |
| Prompt lock | GIT_TERMINAL_PROMPT=0 | Plus GH_PROMPT_DISABLED=1, GH_PAGER=cat | Fail missing credentials in under 5 seconds |
| Limit | What trips it | What this recipe does |
|---|---|---|
| 50 MiB warning | One blob in regular Git | Prefer Git LFS |
| 100 MiB block | One blob in regular Git | Must use Git LFS |
| 2 GiB pack | One git push HTTP pack | Push earlier then later SHAs |
http.postBuffer | Client HTTP buffer | Does not raise GitHub's 2 GiB cap |
| Recommended repo size | About 1 GB, under 5 GB | Keep history small. Binaries in LFS |
When we omitted --private, gh waited on Visibility until timeout. Abort if gh api user is empty. Use one-shot git -c flags, not --global. If git count-objects -vH prints GiB, stop.
How do you implement the step-by-step recipe?
Run these from $HOME/push-2gb-limit-scratch. Always -m on commit and --private on create. Create an empty remote named with $USER and a unix timestamp so a second run does not collide. Do not pass --source or --push. Do not rebase, --force, +SHA, --web, or dd.
export GH_PROMPT_DISABLED=1 GH_PAGER=cat GIT_TERMINAL_PROMPT=0-
Confirm Git, GitHub CLI, Git LFS, and an authenticated login. Auth must already exist. Never
--web.git --version && gh --version && git lfs version && gh api user --jq .loginExpected: Git 2.39+, git-lfs 3.0+, and a login. Missing binaries:
brew install git gh git-lfsorsudo apt-get update -y && sudo apt-get install -y git gh git-lfs. Ifgh api userfails, stop. -
Create six tiny local commits and measure object size. Keep objects in KiB.
mkdir -p "$HOME/push-2gb-limit-scratch"cd "$HOME/push-2gb-limit-scratch"if [ -d .git ]; then echo "ABORT local git dir exists"; exit 1; figit init -b mainn=1while [ "$n" -le 6 ]; do printf 'batch %s\n' "$n" > "note-${n}.txt" git add "note-${n}.txt" git -c user.name='Push Limit Scratch' -c user.email='scratch@example.invalid' \ commit -m "Add note ${n}" n=$((n + 1))donegit symbolic-ref --short HEADgit rev-list --count HEADgit count-objects -vHtest -z "$(git remote)"Expected:
main,6,size: 72.00 KiB. Iforiginis listed, stop. -
Create an empty private GitHub repository and add
originwithout pushing. Skipgh repo create --source --push. That is the one-shot path from the push how-to.cd "$HOME/push-2gb-limit-scratch"OWNER="$(gh api user --jq .login)"REPO="push-2gb-limit-scratch-${USER}-$(date +%s)"if gh repo view "${OWNER}/${REPO}" >/dev/null 2>&1; then echo "ABORT GitHub repo ${OWNER}/${REPO} already exists"; exit 1figh repo create "$REPO" --private \ --description "Throwaway 2 GiB push-limit drill. Safe to delete."git remote add origin "https://github.com/${OWNER}/${REPO}.git"git remote -vgit ls-remote origin refs/heads/mainecho "${OWNER}/${REPO}"Expected:
originset,git ls-remote origin refs/heads/mainempty, uniqueOWNER/REPOprinted. -
Push an earlier SHA to
refs/heads/main, then later SHAs untilHEAD. GitHub samples every 1000th commit withawk 'NR % 1000 == 0'. This scratch uses commits 2, 4, and 6. Full SHAs. No+. Each push must fast-forward.cd "$HOME/push-2gb-limit-scratch"SHA2="$(git log --reverse --format=%H refs/heads/main | sed -n '2p')"SHA4="$(git log --reverse --format=%H refs/heads/main | sed -n '4p')"HEAD_SHA="$(git rev-parse HEAD)"git push origin "${SHA2}:refs/heads/main"test "$(git ls-remote origin refs/heads/main | awk '{print $1}')" = "$SHA2"git push origin "${SHA4}:refs/heads/main"test "$(git ls-remote origin refs/heads/main | awk '{print $1}')" = "$SHA4"git push origin "${HEAD_SHA}:refs/heads/main"git branch --set-upstream-to=origin/main maintest "$(git ls-remote origin refs/heads/main | awk '{print $1}')" = "$HEAD_SHA"test "$(git rev-parse origin/main)" = "$HEAD_SHA"echo batch-okExpected:
* [new branch]then two fast-forwards (1.548 s, 1.606 s, 2.019 s), thenbatch-ok. Do not push SHA4 before SHA2. Omit GitHub's+SHAforce prefix. -
Track a tiny blob with Git LFS so large files never enter the pack. Batch push splits history. It does not raise the 100 MiB file block. Write 5 bytes, never a gigabyte.
cd "$HOME/push-2gb-limit-scratch"git lfs install --localgit lfs track "*.bin"printf 'tiny\n' > demo.bingit add .gitattributes demo.bingit -c user.name='Push Limit Scratch' -c user.email='scratch@example.invalid' \ commit -m "Track a tiny LFS pointer"git lfs ls-filesgit count-objects -vHgit push origin HEAD:refs/heads/maintest "$(git rev-parse HEAD)" = "$(git ls-remote origin refs/heads/main | awk '{print $1}')"echo lfs-okExpected:
demo.binon LFS,88.00 KiB,lfs-ok. The LFS handshake can exceed 5 seconds;git lfs ls-filesandgit count-objects -vHdo not.
On a real import, use GitHub's awk 'NR % 1000 == 0' sampler and drop the step until each pack stays under 2 GiB, then push HEAD. Skip --mirror here: it deletes remote refs you lack locally.
How do you verify the deployment works?
Run this probe from any directory. It must finish in under 5 seconds and print VERIFY_OK.
export GH_PROMPT_DISABLED=1 GH_PAGER=cat GIT_TERMINAL_PROMPT=0cd "$HOME/push-2gb-limit-scratch"test -d .git || { echo VERIFY_FAIL missing_git_dir; exit 1; }test "$(git symbolic-ref --short HEAD)" = "main" || { echo VERIFY_FAIL branch; exit 1; }test "$(git remote)" = "origin" || { echo VERIFY_FAIL remote; exit 1; }test "$(git rev-list --count HEAD)" = "7" || { echo VERIFY_FAIL commit_count; exit 1; }HEAD_SHA="$(git rev-parse HEAD)"test "$HEAD_SHA" = "$(git ls-remote origin refs/heads/main | awk '{print $1}')" || { echo VERIFY_FAIL ls_remote; exit 1; }test "$HEAD_SHA" = "$(git rev-parse origin/main)" || { echo VERIFY_FAIL origin_main; exit 1; }git lfs ls-files | grep -q demo.bin || { echo VERIFY_FAIL lfs; exit 1; }git count-objects -vH | grep -E 'size.+ KiB' >/dev/null || { echo VERIFY_FAIL size; exit 1; }echo VERIFY_OKecho "$HEAD_SHA"Expected stdout: VERIFY_OK and a 40-character SHA.
When we ran this probe at ZeroShot Studio, git ls-remote matched HEAD in 0.4 seconds. Leave the unique private repo. Do not gh repo delete from this recipe.
What are the common production failure modes?
Abort. Do not rebase, --force, or dd.
remote: fatal: pack exceeds maximum allowed sizeorfatal: the remote end hung up unexpectedly: One push packed more than 2 GiB. Fix: reduceawk 'NR % 1000 == 0'to 100 or 50 and pushSHA:refs/heads/mainagain. Do not raisehttp.postBuffer. Do not create a 2 GB file to confirm the error.- GitHub's
+SHAforce refspec orgit push --force: The docs showgit push REMOTE +SHA:refs/heads/BRANCH.+force-updatesmain. Fix: omit+. Push earlier SHAs first. Never--forceonto a shared branch. - 100 MiB file block mistaken for the 2 GiB pack cap: GitHub warns at 50 MiB and blocks a regular Git blob at 100 MiB. Batch push cannot smuggle that blob. Fix:
git lfs install --local,git lfs track, add.gitattributesbefore the binary. - Single commit already larger than 2 GiB: GitHub's page says interactive rebase. This recipe refuses rebase. Fix: if you can drop history, remove
.git,git init -b main,git lfs install, copy.gitattributesfirst, then commit and push file batches under 2 GiB. Do notdd. - Visibility, login, or a populated remote: Skip
--privateandghopens a picker. Missing credentials wait onUsername for 'https://github.com'.--source --pushsends the whole pack. Fix: three locks,--private, addoriginwith no--push. Ifgh api userfails, stop. Never--web. Iforiginalready exists, abort.
FAQ
What is the difference between the 2 GiB push limit and the 100 MiB file limit?
The 2 GiB cap is one git push pack. The 100 MiB cap is one blob in regular Git. GitHub warns at 50 MiB. Batch refspecs split the pack. Git LFS is the path for the blob. See About large files on GitHub.
Why not git push origin main like the first-push how-to?
That command sends every missing object in one pack. Fine for a small history. On a large import it hits pack exceeds maximum allowed size. Push an earlier SHA first. See git-push.
Why omit the + in GitHub's +SHA:refs/heads/BRANCH sample?
+ is a force refspec. It can move main to an ancestor. This recipe only fast-forwards empty private main. Never + or --force onto a shared branch.
When do I use Git LFS instead of batch push? Batch push when history is large but commits are normal Git objects. Git LFS when the blob itself is large. You can use both.
What if one commit is already over 2 GiB and I cannot drop history?
GitHub's page says interactive rebase. This recipe does not rebase. If you cannot drop .git and start from scratch with LFS, stop. Do not --force a rewritten shared branch.