TechEarl

Git: 'File Exceeds GitHub's File Size Limit' - How to Fix It

GitHub rejected your push because a file is over 100 MB. Here is what the warning and the hard limit mean, and how to purge the file from history so the push goes through.

Ishan Karunaratne⏱️ 9 min readUpdated
Share thisCopied
How to fix the GitHub file size limit error when a push is rejected for a file over 100 MB

GitHub rejected your push because one of your files is bigger than 100 MB, GitHub's hard limit for a single file. Deleting the file and committing the deletion will not fix it: the file is still recorded in your commit history, and GitHub checks the whole history of what you are pushing. The real fix is to rewrite history so the big file was never committed, then push again. The fastest way is git filter-repo or BFG Repo-Cleaner, and for files you genuinely need to keep, Git LFS is the right tool.

The error you are seeing

When you push and a file is over the limit, GitHub's pre-receive hook declines the push and prints something like this:

text
remote: error: GH001: Large files detected. You may want to try Git Large File Storage - https://git-lfs.github.com.
remote: error: Trace: 0e1f2a...
remote: error: File assets/demo.zip is 123.45 MB; this exceeds GitHub's file size limit of 100.00 MB
To github.com:you/your-repo.git
 ! [remote rejected] main -> main (pre-receive hook declined)
error: failed to push some refs to 'github.com:you/your-repo.git'

The important line names the file and its size: assets/demo.zip is 123.45 MB. That is the file you have to deal with. The pre-receive hook declined line is GitHub's server-side check kicking in; if you want the broader meaning of that wrapper, see pre-receive hook declined. This particular case is always a size problem.

The 50 MB warning vs the 100 MB hard limit

GitHub treats large files in two stages, and it helps to know which one you hit:

SizeWhat GitHub doesPush outcome
Over 50 MBPrints a warning suggesting Git LFSPush succeeds
Over 100 MBBlocks the push with GH001Push rejected

So a 60 MB file gets through with a nag; a 123 MB file gets stopped cold. The error above is the hard 100 MB block. There is no setting on a normal repository to raise it: 100 MB per file is fixed for pushes over the standard Git protocol. Files you genuinely need that are larger than 100 MB have to go through Git LFS instead, which I cover below.

Why deleting the file does not work

This is the part that trips up almost everyone the first time. Your instinct is to do this:

bash
git rm assets/demo.zip
git commit -m "Remove the huge zip"
git push origin main

The push fails again with the same error. Here is why: Git stores history as a chain of commits, and the big file is still sitting inside the commit where you first added it. Deleting it in a new commit just adds a later commit that says "this file is gone now." The earlier commit, with the full 123 MB blob, is still part of what you are trying to push. GitHub inspects every object in the push, finds the oversized blob in that old commit, and rejects the whole thing.

To get past it you have to rewrite history so that the commit which introduced the file no longer contains it. If you want the underlying mental model of how commits chain together, the Git for Beginners guide lays it out, and removing a file from Git without deleting it locally covers the gentler case where the file is small enough to stay in history.

Fix it before the first push (the easy case)

If you have not pushed yet and the bad commit is your most recent one, you can avoid history surgery entirely. Undo the commit, get the file out of the way, and recommit:

bash
# Undo the last commit but keep your changes staged
git reset --soft HEAD~1

# Unstage and ignore the big file
git restore --staged assets/demo.zip
echo "assets/demo.zip" >> .gitignore

# Recommit without the big file
git add .
git commit -m "Add demo assets (zip excluded)"
git push origin main

git reset --soft HEAD~1 rewinds the commit but leaves your work in the staging area (if you want a refresher on how the staging area works, that explains the index this command leaves your files sitting in), so nothing is lost. If you want the full picture of undoing commits, see how to undo the last Git commit and the difference between reset and revert. Adding the file to .gitignore stops it coming back; the .gitignore guide has the pattern syntax.

This easy path only works while the bad commit is recent and unpushed. If the file is buried several commits back, or you already force the issue across branches, you need to scrub history properly.

Purge the file from all of history with git filter-repo

git filter-repo is the tool the Git project itself recommends for rewriting history (it replaced the old, slow, and footgun-prone git filter-branch). Install it first:

bash
# macOS
brew install git-filter-repo

# pip (any platform)
pip install git-filter-repo

Then, from inside the repository, strip the file from every commit:

bash
git filter-repo --path assets/demo.zip --invert-paths

--path names the file, and --invert-paths means "keep everything except this path." After it runs, the blob is gone from every commit in the repo, history and all. filter-repo deliberately removes your origin remote as a safety measure, so add the origin remote back and force-push the rewritten history:

bash
git remote add origin git@github.com:you/your-repo.git
git push --force origin main
text
+ 3f9a2c1...8b21e0d main -> main (forced update)

The force-push is mandatory here: you have rewritten the commits, so their hashes changed, and a normal push would be rejected as a non-fast-forward. If you would rather understand why that rejection happens, I wrote it up in failed to push some refs.

Or use BFG Repo-Cleaner

BFG is a faster, simpler alternative built specifically for this job. It is a Java jar, so you need a JRE installed. The handy part is that BFG can target files by size rather than name, which is perfect when you do not know exactly which file is the culprit:

bash
# Remove every blob larger than 100 MB from history
java -jar bfg.jar --strip-blobs-bigger-than 100M

# BFG leaves the rewrite pending; finalize it
git reflog expire --expire=now --all
git gc --prune=now --aggressive

git push --force origin main

BFG never touches your latest commit (it assumes your current working tree is the state you want), so make sure the file is already deleted in your latest commit before you run it.

A loud warning about rewriting shared history

Both filter-repo and BFG change commit hashes for the affected commits and everything after them. That is fine for a solo repo or a branch nobody else has pulled. But if teammates have already cloned or pulled the branch you are rewriting, the force-push will leave their local history pointing at commits that no longer exist upstream, and their next pull turns into a mess (worth knowing how fetch and pull differ here, since a bare fetch shows them the divergence before a pull tries to merge it).

SituationSafe to force-push the rewrite?
Solo project, or branch only you haveYes
Shared branch, but you can coordinateYes, after telling everyone to re-clone
Shared main that many people trackAvoid; rewrite a fresh branch instead

If others are on the branch, tell them before you force-push, and have them re-clone afterward rather than trying to merge. Where their checkout no longer matches upstream, it is cleaner for them to discard the local changes instead and reset onto the rewritten branch than to fight a merge. The Git workflows for teams article covers branch hygiene that keeps you out of this situation in the first place. This is the same blast radius you deal with when removing a leaked secret from history, because that is exactly the same kind of operation.

When to use Git LFS instead

Sometimes the big file is not a mistake. You actually need that 300 MB dataset, video, or model checkpoint in the repo. That is what Git Large File Storage (LFS) is for: it stores the real file contents on a separate large-file server and keeps only a tiny text pointer in your Git history, so the 100 MB push limit never trips.

bash
# One-time per machine
git lfs install

# Track a file type (writes a rule to .gitattributes)
git lfs track "*.zip"

# Commit the .gitattributes change and the file as usual
git add .gitattributes assets/demo.zip
git commit -m "Track zip assets with LFS"
git push origin main

A few things worth knowing before you lean on LFS:

  • It only tracks files committed after you set up tracking. A file already baked into history still has to be purged with filter-repo or BFG first, then re-added under LFS.
  • GitHub gives Free and Pro accounts 10 GiB of LFS storage and 10 GiB of monthly bandwidth; beyond that you pay metered usage (GitHub retired the old pre-paid data packs in favor of paying only for what you use).
  • .gitattributes is what records the tracking rules, so commit it alongside the file or collaborators will not pull the file through LFS.

Here is how the three approaches compare:

ApproachUse whenEffect on history
git reset --soft + .gitignoreThe bad commit is recent and unpushedRewrites only the last commit
git filter-repo / BFGThe file is wrong and should never have been committedRewrites all affected history; force-push needed
Git LFSYou genuinely need the large file in the repoNo purge needed for new files; pointer kept in history

Verify the file is really gone

After a filter-repo or BFG rewrite, confirm the blob is no longer in your history before you trust the fix:

bash
git log --all --oneline -- assets/demo.zip

If that prints nothing, the file is gone from every commit and your push should now succeed. If it still lists commits, the rewrite did not catch the path you expected; double-check the exact path (case and folder included) and run it again. You can also recover from a botched rewrite while the old commits are still in your reflog, which is covered in recovering lost commits with git reflog.

FAQ

Sources

Authoritative references this article was fact-checked against.

TagsGitHub file size limitGitVersion ControlGit LFSgit filter-repo

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts