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.

  • #monorepo
  • #git
  • #performance
  • #scaling

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:

SymptomLikely causeFix
git status takes secondsWorking-tree walkfsmonitor
IDE feels laggyRepeated status pollingfsmonitor + cache
git fetch takes minutesObject densitypartial clone + prefetch
git checkout is slowTree materializationsparse checkout
git log is slowHistory walkcommit-graph
CI clone is slowNetwork + object countpartial 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 fetch measurably. 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-repo if 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.