Back to Resources

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
6 linescompact
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 --> Success
Rendered from Mermaid source with the native ZeroLabs diagram container.

What 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 LayerMinimum VersionProduction RecommendationPurpose in Stack
Git binary2.39.02.50+init -b, commit, SHA refspec push, ls-remote, count-objects -vH
GitHub CLIgh 2.40.0gh 2.98+repo create --private with no --push
Git LFSgit-lfs 3.0.0git-lfs 3.7+Pointers so blobs never enter the Git pack
Auth sessiongh api user returns a loginToken already in gh authCreate the private repo without a browser
Prompt lockGIT_TERMINAL_PROMPT=0Plus GH_PROMPT_DISABLED=1, GH_PAGER=catFail missing credentials in under 5 seconds
LimitWhat trips itWhat this recipe does
50 MiB warningOne blob in regular GitPrefer Git LFS
100 MiB blockOne blob in regular GitMust use Git LFS
2 GiB packOne git push HTTP packPush earlier then later SHAs
http.postBufferClient HTTP bufferDoes not raise GitHub's 2 GiB cap
Recommended repo sizeAbout 1 GB, under 5 GBKeep 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.

Terminalbash
export GH_PROMPT_DISABLED=1 GH_PAGER=cat GIT_TERMINAL_PROMPT=0
  1. Confirm Git, GitHub CLI, Git LFS, and an authenticated login. Auth must already exist. Never --web.

    Terminalbash
    git --version && gh --version && git lfs version && gh api user --jq .login

    Expected: Git 2.39+, git-lfs 3.0+, and a login. Missing binaries: brew install git gh git-lfs or sudo apt-get update -y && sudo apt-get install -y git gh git-lfs. If gh api user fails, stop.

  2. Create six tiny local commits and measure object size. Keep objects in KiB.

    Terminalbash
    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. If origin is listed, stop.

  3. Create an empty private GitHub repository and add origin without pushing. Skip gh repo create --source --push. That is the one-shot path from the push how-to.

    Terminalbash
    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: origin set, git ls-remote origin refs/heads/main empty, unique OWNER/REPO printed.

  4. Push an earlier SHA to refs/heads/main, then later SHAs until HEAD. GitHub samples every 1000th commit with awk 'NR % 1000 == 0'. This scratch uses commits 2, 4, and 6. Full SHAs. No +. Each push must fast-forward.

    Terminalbash
    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-ok

    Expected: * [new branch] then two fast-forwards (1.548 s, 1.606 s, 2.019 s), then batch-ok. Do not push SHA4 before SHA2. Omit GitHub's +SHA force prefix.

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

    Terminalbash
    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-ok

    Expected: demo.bin on LFS, 88.00 KiB, lfs-ok. The LFS handshake can exceed 5 seconds; git lfs ls-files and git count-objects -vH do 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.

Terminalbash
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 size or fatal: the remote end hung up unexpectedly: One push packed more than 2 GiB. Fix: reduce awk 'NR % 1000 == 0' to 100 or 50 and push SHA:refs/heads/main again. Do not raise http.postBuffer. Do not create a 2 GB file to confirm the error.
  • GitHub's +SHA force refspec or git push --force: The docs show git push REMOTE +SHA:refs/heads/BRANCH. + force-updates main. Fix: omit +. Push earlier SHAs first. Never --force onto 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 .gitattributes before 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 .gitattributes first, then commit and push file batches under 2 GiB. Do not dd.
  • Visibility, login, or a populated remote: Skip --private and gh opens a picker. Missing credentials wait on Username for 'https://github.com'. --source --push sends the whole pack. Fix: three locks, --private, add origin with no --push. If gh api user fails, stop. Never --web. If origin already 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.

Share