Large Files with Git LFS
Git was built for text: source code, HTML, configuration. It handles thousands of small text files beautifully. Big binary files are a different story. Every version of a 200 MB video or a Photoshop design you commit stays in the history forever, and every clone downloads all of them. A repository can grow to gigabytes in a few months, and GitHub refuses single files above a fixed size.
Git LFS (Large File Storage) is the standard answer. In this lesson you will see why large files hurt, how LFS replaces them with small pointer files, how to set it up and move existing files into it, and when a different tool is the better choice.
Why big binary files hurt a repository
Git stores the full content of every version of every file. For text files this is cheap, because Git compresses well and stores similar versions as small differences. Binary files such as .psd, .mp4 or .zip barely compress and change completely between versions. So:
- Every version stays forever. Deleting the file in a new commit does not shrink the repository; the old versions are still in history.
- Every clone downloads everything. A new teammate in Kochi on a slow connection waits for gigabytes of old banners they will never open.
- Hosting limits apply. GitHub warns about files larger than 50 MiB and blocks files larger than 100 MiB from being pushed. Files added through the browser are limited to 25 MiB.
How Git LFS works
LFS is a Git extension, a separate program that plugs into Git. For the file types you choose, it changes what goes into the repository:
- When you
git adda tracked file, LFS stores the real content in a local LFS folder (inside.git/lfs) and gives Git a tiny pointer file instead. - When you push, LFS uploads the real content to an LFS server. GitHub, GitLab and Bitbucket provide one alongside your repository.
- When you clone or switch branches, LFS downloads the content for the commit you check out and puts the real file in your folder.
Your working folder always shows the real banner, but the Git history contains only small pointers. Clones download only the versions you actually check out, not every version ever made.
Setting up LFS
Git for Windows includes Git LFS. On macOS and Linux, install the git-lfs package from your package manager (or from git-lfs.com), then run git lfs install once. It configures Git's filters for your user account and installs LFS hooks in the current repository:
git lfs installUpdated Git hooks. Git LFS initialized.
Now tell LFS which files to manage. Priya's sweet-shop project gets designs from a freelancer in Jaipur and short promotional videos:
git lfs track "*.psd" "*.mp4"
cat .gitattributesTracking "*.psd" Tracking "*.mp4" *.psd filter=lfs diff=lfs merge=lfs -text *.mp4 filter=lfs diff=lfs merge=lfs -text
git lfs track writes its rules into .gitattributes (see .gitattributes & Line Endings). filter=lfs sends these files through LFS, and -text stops Git from converting line endings in them. Always put the quotes around the pattern, so your shell does not expand *.psd into the files that happen to exist today. And commit .gitattributes, or your teammates' Git will not know these files belong to LFS.
Committing a large file
From here on, you use ordinary Git commands. Priya adds a 3 MB banner design:
git add .gitattributes banner.psd
git commit -m "Add Diwali banner design"
git lfs ls-files --size[main 1cb8b7d] Add Diwali banner design 2 files changed, 5 insertions(+) create mode 100644 .gitattributes create mode 100644 banner.psd 82d58a34a4 * banner.psd (3.1 MB)
Notice that the commit says only a few lines were added. git lfs ls-files lists the files LFS manages with the start of each content hash; the * means the full file is present locally.
Looking at a pointer file
What did Git actually store? Ask Git for the version of banner.psd inside the commit:
git show HEAD:banner.psdversion https://git-lfs.github.com/spec/v1 oid sha256:82d58a34a453397bd9951b61026d7a228b4a0af03c243c9061c15d50362c5180 size 3145728
That is the entire pointer: the LFS specification version, the SHA-256 hash of the real content (oid) and its size in bytes (3,145,728 bytes = 3 MiB). Git versions, diffs and merges these three lines, while the 3 MB file lives on the LFS server.
When you push, LFS uploads the content first, then Git pushes the commits. You will see a line such as Uploading LFS objects: 100% (1/1), 3.1 MB before the usual push output.
Cloning without downloading everything
Sometimes you need the code but not the designs, for example in a CI job that only runs tests. Set the environment variable GIT_LFS_SKIP_SMUDGE=1 while cloning, and LFS leaves the pointer files in place instead of downloading content:
GIT_LFS_SKIP_SMUDGE=1 git clone https://github.com/priya-sharma/sweet-shop.git
git lfs pull --include="banner.psd"After the clone, banner.psd on disk is the three-line pointer. git lfs pull downloads real content later; --include limits it to the files you name.
Moving existing files into LFS
If big files are already in your history, adding a track rule only affects new commits; the old versions still weigh down every clone. git lfs migrate can rewrite history so the old commits use pointers too. First see what is large:
git lfs migrate info --everything*.zip 2.1 MB 1/1 file 100% *.js 6 B 2/2 files 100%
Then convert the zip files in every branch:
git log --oneline
git lfs migrate import --include="*.zip" --everything
git log --oneline8855ae2 (HEAD -> main) Update app d29a7d8 Add product photos c79bddf Add app 97fe229 (HEAD -> main) Update app 37cf4bc Add product photos 70e8c78 Add app
(LFS also prints progress lines such as Rewriting commits: 100% (3/3), done., left out here.) Every hash changed: this is a history rewrite, just like a rebase. Afterwards you must force-push the rewritten branches, and every teammate must re-clone or reset their copies. Agree on a time with the whole team first. Leaked Secrets: Rotate, Then Rewrite explains how to coordinate a rewrite safely. If you cannot rewrite history, git lfs migrate import --no-rewrite moves files into LFS in a new commit without touching old ones.
Locking files that cannot be merged
Two designers editing the same .psd cannot merge their work like code. LFS file locking lets one person claim a file. Mark the pattern lockable with git lfs track --lockable "*.psd", then git lfs lock banner.psd before editing and git lfs unlock banner.psd when done. Lockable files are read-only on disk until you lock them, and LFS refuses to push changes to a file someone else has locked. Locking needs a server that supports it; GitHub does.
When not to use LFS
LFS is for large files that belong with the code and change now and then: design sources, game assets, test fixtures, small models. It is the wrong tool for:
- Build output such as
.apk,.exeordist/bundles. Rebuild them from source and attach finished builds to a GitHub release instead (see Tags & Releases). - Dependencies such as
node_modulesorvendor. Ignore them and install from the lock file. - Huge datasets and backups. Use object storage (a cloud bucket) or a data versioning tool, and store only a small sample in Git.
- Secrets. LFS content is as readable as any other file in the repository.
Common mistakes
- Tracking after committing.
git lfs trackdoes not change past commits. Set it up first, or usegit lfs migrate. - Forgetting to commit
.gitattributes. Teammates then commit the real binary into Git, not a pointer. - Patterns without quotes.
git lfs track *.psdmay expand to today's file names only. - Cloning without LFS installed. You get pointer files instead of images and the app breaks in strange ways. Install LFS and run
git lfs pull. - Using LFS as a free file store. Quotas apply. Keep only what the project needs.
Key points
- Big binary files bloat history forever; GitHub blocks files larger than 100 MiB in normal Git.
- Git LFS stores the real content on an LFS server and commits a small pointer with a SHA-256
oidandsize. git lfs installonce, thengit lfs track "<pattern>", and commit.gitattributes.git lfs ls-fileslists LFS files;GIT_LFS_SKIP_SMUDGE=1andgit lfs pullcontrol downloads.git lfs migrate importrewrites history to move old files into LFS, so it needs a force-push and team agreement.- Do not use LFS for build output, dependencies, huge datasets or secrets.
Exercise
In a new practice repository, set up LFS for *.mp4 and *.pdf files. Create a fake 2 MB PDF with any tool (for example a short Python script that writes random bytes to menu.pdf), commit it, and then prove that Git stored only a pointer. Finally, list the LFS files with their sizes.
Show solution
git lfs install
git lfs track "*.mp4" "*.pdf"
python -c "import os; open('menu.pdf', 'wb').write(os.urandom(2 * 1024 * 1024))"
git add .gitattributes menu.pdf
git commit -m "Add printable menu"
git show HEAD:menu.pdf
git lfs ls-files --sizegit show HEAD:menu.pdf prints three lines: the spec version, oid sha256:... and size 2097152. That pointer is all Git stored; the 2 MB content sits in .git/lfs until it is pushed to the LFS server.
Quick Quiz
1. What does Git store in a commit for a file tracked by Git LFS?
The real content goes to LFS storage; Git versions only the pointer.
2. Where does git lfs track "*.psd" save its rule?
The rule is an attribute line with filter=lfs; committing it shares it with the team.
3. Your repository already has large zip files in old commits. What does git lfs migrate import --include="*.zip" --everything do?
It is a history rewrite, so it needs a force-push and coordination with the team.
4. Which of these is a good use of Git LFS?
Design sources belong with the project; build output and dependencies should be rebuilt or installed instead.
Learn Git with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
