Monorepo Git Performance: A Practical Guide
Everything we've learned shipping git tooling for million-file monorepos — what scales, what doesn't, and the order to apply each fix.
Monorepos catch a lot of unfair blame for being slow. The truth is that Git scales surprisingly well — if you know which knobs to turn. This is the playbook we’d hand a team adopting a monorepo today, in the order the wins compound.
Triage: what’s actually slow?
Before you optimize anything, profile a single developer day. The common pain points and their root causes:
| Symptom | Likely cause | Fix |
|---|---|---|
git status takes seconds | Working-tree walk | fsmonitor |
| IDE feels laggy | Repeated status polling | fsmonitor + cache |
git fetch takes minutes | Object density | partial clone + prefetch |
git checkout is slow | Tree materialization | sparse checkout |
git log is slow | History walk | commit-graph |
| CI clone is slow | Network + object count | partial clone |
Each of these has a different fix. Lumping them together as “Git is slow” leads to bad decisions like “let’s split the repo.” Almost always you have one or two specific pain points, and each has a known cure.
Step 1: Enable fsmonitor
If git status is slow, this is the single biggest lever. Install gity (or any v2-compatible helper) and register the repo:
cargo install gity
gity register ~/work/monorepo
You’re done. Every subsequent git status, git diff, and IDE poll runs against the daemon’s warm cache. See our deep dive on the fsmonitor protocol for what’s happening under the hood.
Step 2: Enable the manyFiles feature bundle
git config feature.manyFiles true
This sets several flags at once: core.untrackedCache=true, index.version=4, index.skipHash=true. The combined effect is a smaller index (30–50% smaller in our measurements) and faster cold-start latency.
feature.manyFiles is safe to enable on any repo. It is not opt-in by default only because Git’s compatibility policy requires that older versions can still read repos written by newer ones.
Step 3: Partial clone for CI
CI runners that clone the whole repo are often the most expensive part of a build. Partial clone solves this:
git clone --filter=blob:none https://github.com/org/monorepo.git
--filter=blob:none clones everything except file contents. Blobs are fetched lazily, only when something actually needs them. For a CI job that builds one subdirectory, this can reduce clone time by 90%.
--filter=tree:0 is even more aggressive — it skips trees too, fetching them on demand. Use with care: anything that walks history (like git log --follow) becomes a series of synchronous remote round-trips.
Step 4: Sparse checkout for developers
If your team works on a small subset of the monorepo, sparse checkout limits the working tree to just the directories you care about:
git sparse-checkout init --cone
git sparse-checkout set apps/payments libs/shared
In cone mode, sparse checkout is fast and predictable: you specify directories, and only those (plus their parents up to root) get materialized. Random files at the root (READMEs, configs) always materialize too, so nothing depends on path-dependent magic.
For Bazel- or Pants-shaped monorepos where each engineer works on one or two leaf projects, sparse checkout is the single biggest disk-usage and checkout-time win.
Step 5: Background maintenance
Git has shipped a built-in scheduler since 2.29:
git maintenance start
This schedules prefetch (download new objects in the background), commit-graph rebuild (so git log stays fast), and incremental repack (so the pack files don’t grow unboundedly).
With gity, you get the same thing — but scheduled across every registered repo, throttled when the system is under load, and paused on battery power.
Step 6: Commit-graph and reachability bitmaps
If git log is slow, your repo has hit a history-walk wall.
git commit-graph write --reachable --changed-paths
The commit-graph file accelerates reachability queries (the work git log, git fetch, and git push all rely on). With --changed-paths, queries like git log -- path/to/file get much faster too.
Reachability bitmaps are the next level up — they let Git answer “which objects are reachable from X?” in O(1) instead of O(history). Enable on the server side with git repack -A --write-bitmap-index; the bitmaps download with clones and accelerate every clone after the first.
What doesn’t scale
A few things to know going in:
- Branches: tens of thousands of refs slow down
git fetchmeasurably. Hosted services like GitHub handle this fine on the server side, but local performance degrades. Prune dead branches. - Large files in history: a 500MB binary that got committed two years ago still gets cloned on every fresh checkout. Use Git LFS for assets, and rewrite history with
git filter-repoif a giant slipped through. - Submodules: they technically work but produce a worse experience than monorepos for shared libraries. If you reach for submodules, ask first whether you actually want a monorepo with sparse checkout.
When to consider splitting
We meet roughly one team a quarter that genuinely should split their monorepo. The pattern is consistent:
- Two or more teams with no shared code paths, independent release cycles, and different security postures.
- A repo that has grown to hundreds of millions of objects and where rebuilding history (e.g., a security incident requiring rewrite) is genuinely impossible.
If you’re not in one of those situations, fix the performance instead. Splitting hurts dependency management, CI matrices, refactors, and developer onboarding — all of which compound for years.
Try it
cargo install gity
gity register .
git config feature.manyFiles true
git maintenance start
Four commands. On a 250k-file monorepo, this is typically the difference between “Git is slow” and “I forget Git is even there.”
Frequently asked questions
At what size does Git get slow?
It depends on the operation. Status starts feeling slow above ~50,000 tracked files. Clone and fetch start to hurt above a few million objects. History walks (`git log -p`) get expensive above ~100,000 commits. Most teams hit the status problem first.
Should I split my monorepo?
Usually no. Modern Git scales much better than its reputation suggests — every major tech company runs monorepos on plain Git with fsmonitor, partial clone, and sparse checkout. Splitting incurs huge ongoing coordination costs and rarely pays back.
What's the order to apply performance fixes?
Enable fsmonitor (gity or equivalent), turn on `feature.manyFiles`, enable partial clone (`--filter=blob:none`) for CI, then sparse checkout for developers who touch only a subset. Background maintenance (prefetch, repack) goes on after the steady state is fast.