You committed a 250 MB dataset, realized the mistake, deleted it, committed the deletion, and ran git push origin main — only to hit:
remote: error: File dataset.csv is 250.00 MB; this exceeds GitHub's file size limit of 100.00 MB
remote: error: GH001: Large files detected. You may want to try Git Large File Storage - https://git-lfs.github.com.
Fastest fix: deleting a file in a later commit does not remove it from history — the blob is still referenced by the old commit, so every push (and every clone) carries it. You have to surgically remove the file from every commit with git-filter-repo, which rewrites the commit SHAs, then force-push. Because rewriting history is destructive and changes every SHA, coordinate with anyone who has a clone first.
The limits, exactly (as of June 2026)
GitHub measures in mebibytes (MiB), not decimal MB, and applies three different thresholds. Knowing which one you tripped tells you what to do.
| Threshold | What happens | Source |
|---|---|---|
> 50 MiB | Git prints a warning, but the push still succeeds | GitHub Docs |
> 100 MiB | Push is blocked with GH001 (this article’s error) | GitHub Docs |
> 25 MiB via web “Add file” UI | Browser upload rejected (this is a separate, stricter limit) | GitHub Docs |
Repo > ~5 GB total | Soft limit; GitHub may email you to slim it down | Repository limits |
If you only need files over the limit going forward, Git LFS (Large File Storage) or GitHub Releases is the right home — but LFS does not retroactively clean a blob already baked into history. That still needs the rewrite below.
GitLab, Bitbucket, and Azure DevOps enforce similar per-file caps; the rewrite procedure is identical, only the error wording differs.
Common causes
Ordered by hit rate, highest first.
1. Large dataset or model weights committed directly
Data and ML teams often commit CSVs, HDF5 files, Parquet, or .onnx / .safetensors model weights straight into the repo for “convenience,” not realizing the 100 MiB cap or the clone-size cost.
How to spot it: run the blob-size scan in Step 2 below; the offender is usually a single .csv, .h5, .parquet, or weights file at the top of the list.
2. Build artifact accidentally committed
A compiled binary (*.jar, *.war, *.exe, dist/*.js.gz) over 100 MiB landed in a “full release” commit. CI now fails on every fresh clone.
How to spot it: the extension is missing from .gitignore. List large tracked files in the working tree:
git ls-files | xargs -I{} du -h {} 2>/dev/null | sort -rh | head -10
3. Database dump or log file committed
pg_dump production.sql (2 GB) committed “temporarily” for a migration and never pruned before the next commit.
How to spot it: extension is .sql, .log, .dump, or .bak. Check the blob size of a known path:
git cat-file -s HEAD:production.sql # prints byte size of that blob
4. LFS migration started but not finished
Someone ran git lfs track "*.psd" and committed .gitattributes, but the existing .psd blobs already in history were never converted to LFS pointers.
How to spot it: git lfs ls-files shows nothing, yet .gitattributes has LFS rules. The big blobs are still regular Git objects. Confirm what is still un-migrated:
git lfs migrate info --everything --above=50MB
5. Submodule or vendored repo bundled instead of referenced
A developer copied a dependency repo into the project directory and committed the whole thing — sometimes including its .git objects as loose files.
How to spot it: the Step 2 scan lists many large blobs under one directory (e.g. vendor/, third_party/).
Shortest path to fix
Step 1: Install git-filter-repo (preferred over filter-branch and BFG)
git filter-repo is the tool the Git project itself recommends; it is faster than the old git filter-branch and handles edge cases BFG does not.
pip install git-filter-repo
# macOS: brew install git-filter-repo
# Debian/Ubuntu: apt install git-filter-repo
git filter-repo --version # confirm it's on PATH
Step 2: Find every large blob across all history
git rev-list --objects --all \
| git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \
| awk '/^blob/ { print $3, $4 }' \
| sort -rn \
| head -20
Sizes are in bytes; 134217728 is 128 MiB. Note the paths of everything over 50 MB — you will pass them to filter-repo.
Step 3: Back up the repository before rewriting
This rewrite is irreversible. Make a copy you can fall back to.
cd ..
cp -r myrepo myrepo-backup
cd myrepo
git tag backup/before-filter-repo # a second safety net inside the repo
Step 4: Remove the file(s) from all history
Remove a single path:
git filter-repo --path dataset.csv --invert-paths --force
Remove several paths at once:
git filter-repo \
--path dataset.csv \
--path models/weights.onnx \
--path dumps/production.sql \
--invert-paths --force
Or remove everything over a size regardless of name (handy when many files tripped the limit):
git filter-repo --strip-blobs-bigger-than 100M
--force is required because you are running on an existing checkout rather than a fresh clone; that is why the Step 3 backup matters. After it runs, git count-objects -vH should show the repo has shrunk.
Step 5: Add the file to .gitignore so it can’t return
echo "dataset.csv" >> .gitignore
echo "*.onnx" >> .gitignore
git add .gitignore
git commit -m "chore: ignore large files that must not be committed"
Step 6: Re-add the remote and force-push
git filter-repo deletes the origin remote on purpose, as a guard against accidentally re-pushing to a shared repo before you mean to. Re-add it:
git remote add origin <remote-url>
Now push. On this first push after a rewrite there is no remote-tracking ref for --force-with-lease to compare against, so the lease check has nothing to protect and a plain --force is what actually works:
git push --force --all
git push --force --tags
(On subsequent rewrites, once the remote-tracking refs exist again, prefer --force-with-lease so you don’t clobber a teammate’s new work.)
If a protected branch rejects the force-push, temporarily lift “Allow force pushes” / branch protection in Settings -> Branches (GitHub) for that branch, push, then re-enable it.
Step 7: How to confirm it’s fixed
- The push completes with no
GH001/ “exceeds … file size limit” error. - Re-run the Step 2 scan — the offending path is gone and the top blob is now under 100 MiB.
git count-objects -vHreports a smallersize-pack.- Fresh-clone test:
git clone --depth 1 <remote-url> /tmp/verify-clonesucceeds quickly and the file is absent.
Then tell every teammate: their clones still contain the old SHAs and will re-introduce the big file if they push. The clean fix is a fresh clone. If they have local-only work, they should instead run:
git fetch --all
git rebase --onto origin/main ORIG_HEAD main
Prevention
- Add a pre-commit hook that blocks oversized files before they ever enter history:
# .git/hooks/pre-commit (chmod +x)
LIMIT=52428800 # 50 MiB
git diff --cached --name-only --diff-filter=AM | while read f; do
size=$(git cat-file -s ":$f" 2>/dev/null || echo 0)
if [ "$size" -gt "$LIMIT" ]; then
echo "ERROR: $f is $((size/1048576)) MiB — exceeds the 50 MiB limit."
exit 1
fi
done
Or, if your team uses the pre-commit framework, enable its built-in check-added-large-files hook (default cap 500 KB, tunable with --maxkb). Hooks live per-clone, so commit a shared install step — see Git hooks don’t run after clone.
- Use Git LFS for binary assets over ~10 MB:
git lfs track "*.psd" "*.onnx" "*.zip". - Store datasets in object storage (S3, GCS, Azure Blob) and reference them by URL or content hash in the repo.
- Commit a
.gitignoreat repo creation covering common large-file extensions and build output (dist/,build/,node_modules/,*.log,*.sql). - Run
git count-objects -vHperiodically; sudden growth insize-packflags an accidental commit early. - For monorepos, enforce a per-blob size budget in CI that fails the build if any committed object exceeds the threshold.
FAQ
Q: I already deleted the file and committed — why does GitHub still reject the push? A: Git stores snapshots, not diffs. Deleting a file adds a new commit where it’s absent, but the old commit still references the blob. A push transfers the whole history, big blob included. Only rewriting history (Steps 4–6) removes it.
Q: Can I use BFG Repo-Cleaner instead?
A: Yes, for simple cases it’s fast: bfg --strip-blobs-bigger-than 100M or bfg --delete-files dataset.csv, followed by git reflog expire --expire=now --all && git gc --prune=now --aggressive. But git filter-repo is the official Git recommendation, is actively maintained, and handles more edge cases, so reach for it first.
Q: After filter-repo my push is rejected, or --force-with-lease does nothing. What now?
A: filter-repo removed origin, so re-add it (git remote add origin <url>). On the first push after a rewrite there’s no remote-tracking ref for the lease to check, so use plain git push --force --all instead of --force-with-lease.
Q: Teammates cloned before I rewrote history. What should they do?
A: A fresh clone is safest. If they have unpushed local commits, they can run git fetch --all then git rebase --onto origin/main ORIG_HEAD main to replay their work onto the rewritten history. They must not git pull and merge — that drags the old big blob back in.
Q: The file was added once months ago and deleted long since. Do I really have to rewrite everything? A: Yes. Even if it was “deleted” in commit 50, the blob is still referenced by commit 5, so removing it requires rewriting commit 5 and everything after it. That’s why all SHAs change.
Q: Should I just raise the limit with git config http.postBuffer?
A: No. http.postBuffer only affects how Git buffers large pushes over HTTP; it does nothing to the server’s 100 MiB per-file rule. Bumping it is only ever appropriate for legitimately large (LFS-tracked) pushes, never as a workaround for a file that shouldn’t be committed.