Managing Large Binaries in GitHub: LFS vs. External Storage vs. Repo Splitting
A decision guide for handling large binary assets in GitHub. Compare Git LFS, external object storage, and repository splitting against size, bandwidth, and access-control constraints.
12 Jul 2025, 08:36 UTC

Large binary files break Git's assumptions. Because Git stores every version of every file as a compressed object, a 150 MB asset updated ten times permanently adds roughly 1.5 GB to the repository history that every clone must download. GitHub applies a hard per-file size limit to standard Git pushes and recommends keeping total repository size bounded; exceeding those limits causes rejected pushes or degraded performance for all collaborators. The practical decision is where to draw the storage boundary: inside Git via Large File Storage (LFS), outside Git in object storage, or in a separate repository.
Decision Constraints
Before comparing mechanisms, fix the constraints that will rule options in or out:
- Repository size and clone time: Standard Git history must stay small enough that a fresh clone finishes quickly on a typical connection.
- Storage and bandwidth quotas: GitHub plans meter Git LFS storage and monthly bandwidth separately. Exhaustion can block pushes or fetches until the quota resets or a larger plan is purchased.
- Access control granularity: Source code and binary assets may need different audiences. A separate repository can carry its own permissions; LFS inherits the parent repository's visibility.
- Workflow integration: Developers expect
git checkoutto produce a working tree. Moving binaries outside Git adds a bootstrapping step.
Supported Options Compared
| Strategy | Storage Boundary | Best For | Key Trade-off |
|---|---|---|---|
| Git LFS | Inside Git history as pointers; binaries in a separate LFS store | Versioned assets that must ship with code (textures, models, design files) | Native workflow, but consumes plan-specific LFS storage and bandwidth quotas |
| External object storage | Outside Git entirely (cloud buckets, artifact registries) | Massive datasets, build outputs, or assets that do not change with every commit | No Git overhead or LFS quotas, but requires custom tooling and loses native diff/blame |
| Repository splitting | Separate Git repository linked via submodule or package reference | Asset libraries versioned independently from application code | Clean history and independent ACLs, but no atomic commits across code and assets |
Trade-offs in Practice
Git LFS: native workflow, metered cost
Git LFS replaces large files in the commit graph with lightweight text pointers. The actual binary lives in an LFS store; on checkout, the pointer resolves to the correct version. Clone speed stays reasonable because history contains only pointers, but every fetch of a new binary consumes bandwidth against your plan's monthly allowance. If a team exceeds the quota, CI jobs that pull LFS objects can fail until the quota resets. LFS also inherits the parent repository's permission model: anyone who can read the repository can read the LFS objects.
External object storage: unlimited scale, custom plumbing
Keeping binaries in cloud object storage or an artifact registry removes them from Git history entirely. There is no LFS quota and no history bloat. The cost is operational: you must write upload/download tooling, manage credentials outside GitHub's permission model, and handle lifecycle policies (retention, encryption, deletion) yourself. GitHub cannot show diffs or blame for files it never sees, so traceability between a code change and an asset version must be maintained manually—often through a manifest file or release tag.
Repository splitting: clean boundaries, coordination cost
A dedicated asset repository preserves normal Git history for binaries and can carry its own access controls. The main code repository stays small. The penalty is atomicity: updating a shader and the code that consumes it requires two commits in two repositories, plus a submodule pointer update. If the pointer is missed, builds break. Cross-repository pull requests and release notes also become harder to correlate.
Concrete Implementation: Configuring Git LFS Tracking
If the decision is Git LFS, configure tracking before the first large file is committed. Retrofitting LFS onto existing history rewrites commit SHAs, invalidates open pull requests, and requires a coordinated force-push across every collaborator.
The following steps assume Git LFS is installed locally and you have write access to the target repository. Run them in your local clone.
-
Initialize the LFS filters in your global Git configuration (once per machine):
git lfs install -
Track the specific binary patterns. For example, to manage Photoshop files under an
assets/directory:git lfs track "assets/**/*.psd"This command appends a pattern to
.gitattributes. Review the file to confirm only intended patterns are managed by LFS; an overly broad pattern such as*would capture source files and waste quota. -
Commit the
.gitattributesfile so every clone uses the same rules:git add .gitattributes git commit -m "Configure LFS tracking for design assets" -
Add the binary files as usual. Git will store pointers in the commit and upload the binaries to the LFS store on push:
git add assets/hero.psd git commit -m "Add hero design asset" git push origin main
Validation and Verification
Configuration mistakes are cheap to catch before they become quota problems.
-
Confirm pointer substitution: Run
git lfs ls-filesin the repository. Files listed here are managed by LFS. If a large binary is missing from the list, it is still in standard Git history. -
Inspect the pointer: Run
git show HEAD:assets/hero.psd. The output should be a small text block starting withversion https://git-lfs.github.com/spec/v1, not binary data. -
Test a clean clone: Clone the repository into an empty directory and check out the branch. LFS should download the binaries during checkout. If the clone succeeds but the file is a pointer text file, LFS is not fetching correctly.
-
Check quota headroom: Open the repository's Billing or Insights pages on GitHub to review current LFS storage consumption and remaining bandwidth for your plan before starting a large migration.
-
Dry-run the largest push: Before committing a particularly large asset, use
git push --dry-runto confirm the push is not rejected by GitHub's file-size limits. Note that--dry-rundoes not upload LFS objects, so it validates Git object acceptance only.
Limitations and Current Verification
GitHub's file-size limits, repository-size recommendations, and LFS storage/bandwidth allowances vary by plan and change over time. The commands above rely on well-established Git LFS behavior, but the specific numeric limits and quota amounts must be verified against GitHub's current published documentation for your plan. If your team is near a quota boundary, monitor the Billing page before large imports rather than discovering the limit during a CI run.
If binaries are already committed to standard Git history, migrating them to LFS is a state-changing operation: commit SHAs change, open pull requests may show unexpected diffs, and all collaborators must re-clone or reset. Treat that migration as a scheduled maintenance window, not a routine commit.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.