Git LFS: How to Store Large Files in a Git Repository
Git LFS (Large File Storage) is a Git extension that stores large binary files on a separate server and keeps only small pointer files in the repository. Clones stay fast, history doesn't bloat, and the workflow stays familiar: git add, commit and push.
What is Git LFS and why do you need it
Git LFS (Large File Storage) is the official Git extension for storing large files: videos, PSD mockups, datasets, ML models, game assets. Instead of the file itself, a text pointer of about 130 bytes goes into the commit, and the content goes to a separate LFS store on the server. On checkout the extension puts the real file into your working directory, so from your point of view it all looks like regular Git.
The problem git lfs solves comes from how Git itself works. Every version of every file stays in the history forever, and cloning downloads the entire history. If you changed a 200 MB video ten times, the repository already weighs about 2 GB, and every new developer downloads all ten versions even if they only need the latest one. Compression and deltas, which work well for text, are almost useless for binary files.
With LFS, a clone only downloads the versions of large files for the current commit. Older versions stay on the server and are fetched when you check out an older commit.
How Git LFS works under the hood
The mechanism is built on Git's clean and smudge filters. When you git add a file tracked by LFS, the clean filter runs: it puts the content into the local cache at .git/lfs/objects and passes a pointer to the index. On checkout the smudge filter does the reverse: it finds the object by hash in the cache (or downloads it from the server) and writes out the real file.
A pointer looks like this:
version https://git-lfs.github.com/spec/v1
oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393
size 209715200
Three lines: the spec version, the SHA-256 of the content, and the size in bytes. That's what gets stored in Git history, so the history stays lightweight. On git push a pre-push hook runs: first the new objects are uploaded via the LFS API, then the commits are sent over the regular protocol.
Which files go to LFS is defined by the .gitattributes file at the root of the repository. It's committed along with the code, so the whole team shares the same rules.
How to install Git LFS
Git for Windows already includes the extension in its installer. On macOS and Linux it's installed as a package:
# macOS (Homebrew)
brew install git-lfs
# Debian / Ubuntu
sudo apt install git-lfs
# Fedora
sudo dnf install git-lfs
After installing, run this once per user:
git lfs install
The command registers the LFS filters in your global ~/.gitconfig and installs hooks into the current repository if you're inside one. You can check that everything is in place with git lfs version, and git lfs env shows the detailed configuration (server URL, cache paths).
How to start tracking large files
A typical setup for a new project:
git lfs track "*.psd"
git lfs track "*.mp4"
git lfs track "models/**"
git add .gitattributes
git add design/banner.psd
git commit -m "Add banner mockup via LFS"
git push origin main
The quotes around the pattern are required: without them the shell expands *.psd into a list of files in the current folder, and you end up with the wrong rule. After git lfs track, this line appears in .gitattributes:
*.psd filter=lfs diff=lfs merge=lfs -text
The -text attribute disables line-ending conversion, which would corrupt a binary file. Order matters: first the track rule, then git add the file. If a file was added the normal way before the rule existed, it stays in the history as a regular blob.
How to check that a file is actually in LFS
git lfs ls-files
4d7a214614 * design/banner.psd
9c1e0b7a3f * video/intro.mp4
git lfs status
git lfs ls-files lists the files in the current commit that are stored in LFS. An asterisk means the content has been downloaded; a dash means the working copy only has the pointer. Running git lfs track with no arguments lists the tracking patterns, and you can remove a rule with git lfs untrack "*.psd".
Cloning and fetching files
A regular git clone works with LFS transparently: if the extension is installed, large files are downloaded automatically. But if a teammate doesn't have LFS installed, they'll see text pointers instead of images in their working copy — the most common symptom of "my PSD turned into some text". The fix is to install it and run:
git lfs install
git lfs pull
git lfs pull downloads the objects for the current commit and puts them in the working directory. There's also git lfs fetch, which only downloads into the cache without touching files. With the --all flag it downloads objects for all refs, which is handy for a full backup or moving to another server.
How to clone without the large files
In CI, or when you only need the code, downloading gigabytes of assets is wasteful. An environment variable disables the smudge filter:
GIT_LFS_SKIP_SMUDGE=1 git clone https://example.com/team/game.git
cd game
git lfs pull --include="textures/ui/**"
The repository is cloned with pointers, and then you selectively pull the files you need with --include (there's also --exclude). This noticeably speeds up pipeline builds.
How to migrate existing files to Git LFS
A common situation: the repository has already bloated and large files are sitting in the history. git lfs track doesn't apply retroactively — it only affects new commits. To rewrite history, use git lfs migrate.
First, see what's taking up space:
git lfs migrate info --everything --top=10
Then migrate the file types you need across all branches:
git lfs migrate import --include="*.psd,*.mp4" --everything
git push --force --all
git push --force --tags
Migration rewrites every commit that contained these files, so all the hashes change. It's the same story as rebasing published branches: warn everyone involved; after the force push they need to re-clone the repository or hard-reset their branches. Make a backup before migrating (git clone --mirror).
If rewriting history isn't an option, there's a gentler alternative: git lfs migrate import --no-rewrite path/to/file. It creates a new commit that moves the files to LFS from that point on. Old versions remain in the history, but the repository stops growing.
File locking: avoiding conflicts in binary files
Two designers edit the same PSD — Git can't merge their changes, and one person's work gets lost. LFS offers locks for files like these. Mark the type as lockable:
git lfs track "*.psd" --lockable
A lockable attribute is added to .gitattributes, and such files become read-only in the working copy until you lock them:
git lfs lock design/banner.psd
git lfs locks
design/banner.psd anna ID:123
# after pushing your changes
git lfs unlock design/banner.psd
While a file is locked, a push of changes to it from anyone else is rejected. Locking is implemented by the server, so it works on major Git hosting platforms but may be missing on a custom server. An admin can remove someone else's lock with git lfs unlock --force.
Where LFS objects are stored and what it costs
LFS objects live on a server that supports the LFS API. It's implemented by popular Git hosting services and self-hosted solutions, as well as standalone servers. Cloud hosts have limits on LFS storage and bandwidth, and they vary between services and plans — check your platform's current terms before putting hundreds of gigabytes of datasets there.
Keep in mind that bandwidth is counted on every download: a CI pipeline that clones a repository with LFS files on every commit can burn through the quota faster than people do. Cache .git/lfs between builds or use GIT_LFS_SKIP_SMUDGE.
The local cache grows too. git lfs prune deletes old objects that are already on the server and aren't needed for current and recent commits.
When Git LFS is a good fit and when it isn't
| Situation | Use LFS? | Notes |
|---|---|---|
| Game assets, mockups, audio, video | Yes | The classic case: binaries that change often |
| ML models and mid-sized datasets next to the code | Yes, mind the quotas | For very large data, dedicated data versioning tools are more convenient |
| Compiled build artifacts | No | They belong in an artifact registry or releases, not in Git |
| Dependencies (node_modules, vendor) | No | Installed by the package manager; add them to .gitignore |
| Large text files (SQL dumps, CSV) | It depends | Git stores text efficiently as deltas; LFS makes sense if the files are huge and barely readable in a diff |
| Secrets and private keys | Never | LFS doesn't encrypt data; it's not a secrets store |
Common errors and how to fix them
The working copy has a text pointer instead of the file
LFS isn't installed, or the clone was made with GIT_LFS_SKIP_SMUDGE=1. Run git lfs install and git lfs pull.
A file ended up in history without going through LFS
The track rule was added after git add. If the commit hasn't been pushed yet, remove the file from the index (git rm --cached file), make sure the rule is in .gitattributes, and add it again. If it's already pushed, use git lfs migrate import. git lfs fsck --pointers helps find such files.
Push rejected because of file size
Many hosts limit the size of a single object in regular Git. A message like "this exceeds the file size limit" means the file should have gone to LFS. Rewrite the last commit or the history as described above — a simple "delete file" commit won't help, because the file stays in the previous commits.
Everything broke for teammates after git lfs migrate
Their local branches are based on the old hashes. The most reliable fix is to re-clone the repository; if they have unpushed work, move it onto the new branches with cherry-pick.
Git LFS commands cheat sheet
| Command | What it does |
|---|---|
git lfs install | Sets up filters and hooks |
git lfs track "*.ext" | Adds a pattern to .gitattributes |
git lfs untrack "*.ext" | Removes a pattern |
git lfs ls-files | Files in LFS for the current commit |
git lfs pull | Download and check out objects |
git lfs fetch --all | Download objects for all refs into the cache |
git lfs migrate import --include="..." --everything | Move files from history into LFS |
git lfs lock / unlock / locks | Manage file locks |
git lfs prune | Clean old objects out of the local cache |
git lfs env | Diagnose settings |
Summary
Git LFS moves large binary files out of history into separate storage, leaving lightweight pointers in commits. The main rule is to set up git lfs track before the first commit of large files, commit .gitattributes, and keep an eye on your hosting quotas. If the files are already in the history, git lfs migrate helps, but that's a history rewrite with all its consequences. We cover working with history, rebase and other advanced Git tools hands-on in the Git course.
FAQ
Is Git LFS free?
The extension itself is open source and free. You may pay your Git host for storage and bandwidth if you exceed your plan's free limits.
How big does a file have to be to use LFS?
There's no hard threshold; binaries of a few megabytes or more that change often usually go to LFS. Git stores text files efficiently even when they're large, without LFS.
Can I stop using Git LFS and move files back into regular Git?
Yes, with git lfs migrate export --include="*.psd" --everything, which rewrites history back. Then remove the rules from .gitattributes and force push.
Does git diff work for files in LFS?
Git shows the change in the pointer, not the content, because the files are binary. Images and documents are compared with external tools.
Does everyone on the team need LFS?
Yes, otherwise a teammate without the extension gets pointers instead of files in their working copy, and their commits may accidentally add large files that bypass LFS.
This topic is covered in depth with practice in the course “Git from Scratch to Confident Developer”.
Go to course