High Performance Git

Section V · Write Pressure, Diagnosis, and Recovery

Chapter 20

Finding and Fixing Slow Git

Pencil sketch of a sailor making a knot with rope in a harbor setting.

With a slow command, a rough baseline, and one or two trace files, you can now get to the problem and fix things.


Start With the Symptom, Not the Theory

Let's put it all together. We need to know which command is slow, and what kind of work it is doing. As we have said many times, each requires a different fix. Consider these four:

time git status
time git log -- path/to/file
time git switch main   # actually switches — substitute your target branch
time git fetch --dry-run

Those four commands touch four different layers: local filesystem and index work, graph and tree history work, local materialization, and network synchronization.

Separate Local Time From Remote Time

When a command crosses the network, break the delay apart before guessing at fixes. For clone, fetch, and push, the time may include client-side preparation, server-side negotiation, hook or policy checks, transfer time, and local unpack or checkout. Start by timing the split:

time git fetch --dry-run
time git fetch

--dry-run skips some real costs, but it is often enough to separate negotiation and server work from later local unpack and checkout behavior.

What To Run First When You Do Not Know Yet

If the problem is still vague, start with one small instrumentation pass instead of jumping straight to fixes:

mkdir -p perf

git count-objects -vH >perf/count-objects.txt
git config --show-origin --get-regexp '^(core\.fsmonitor|core\.untrackedCache|index\.sparse|fetch\.|maintenance\.|commitGraph\.)' >perf/config.txt || true

time git status >/dev/null
GIT_TRACE2_PERF=perf/status.perf git status >/dev/null

That already answers several practical questions:

If the slow command is fetch or push, swap in the network-oriented version:

time git fetch --dry-run >/dev/null
GIT_TRACE_PACKET=perf/fetch.packet GIT_TRACE2_PERF=perf/fetch.perf git fetch origin >/dev/null

git status Problems Usually Mean Local Filesystem and Index Cost

If the complaint is status, the likely cost layers are tracked-file validation, untracked-file discovery, ignore matching, index read and write cost, and working tree size. That points you toward the index, sparse-index, untracked cache, fsmonitor, split index, and worktree reduction, not toward graph or transport tuning. The questions to ask:

The first evidence pass can stay simple:

time git status
git config --show-origin --get-regexp '^(core\.fsmonitor|core\.untrackedCache|index\.sparse)'
git ls-files --debug | sed -n '1,20p'

Path-Limited History Usually Means Graph and Tree Cost

If the complaint is git log -- path, the likely cost layers are commit walk cost, path-limited history reasoning, changed-path Bloom filters, and rename heuristics. A path-limited history query is a graph walk with path reasoning layered on top, and sometimes rename heuristics on top of that, which is why the fix lives in commit-graph, changed-path Bloom filters, and the history-traversal chapter rather than anywhere near transport. The right questions are:

The corresponding probe:

time git log -- path/to/file
git commit-graph verify
git config --show-origin --get-regexp '^commitGraph\.'

Checkout and Branch-Switch Cost Usually Means Materialization

If the complaint is switch, checkout, or branch movement generally, the likely cost layers are working-tree rewrite volume, index rewrite volume, sparse versus dense checkout layout, and stash churn from repeated context teardown. Slow checkout almost always means too much local materialization rather than bad history storage, which is why the fix lives in worktrees, sparse-checkout, sparse-index, and the earlier command-semantics material. The right questions are:

Start here:

time git switch main
git worktree list
git sparse-checkout list

Clone and Fetch Problems Usually Mean Transport

If the complaint is clone or fetch, the likely cost layers are transfer volume, negotiation cost, server-side pack generation, missing bitmaps, absent bundle seeding, and the lack of a partial-clone strategy. Network diagnosis needs its own layer because many local optimizations do not help a bad clone path; the fix more often lives in protocol v2, bundle URIs, bitmaps, and partial clone. The right questions are:

The first transport pass:

time git fetch --dry-run
git count-objects -vH
git config --show-origin --get-regexp '^(fetch\.|remote\..*promisor|maintenance\.)'

Typical Fixes Map to Typical Layers

Once the issue is known, return to the options we've learned.

Working-tree and index fixes:

History and graph fixes:

Storage fixes:

Transport fixes:

You can compress that mapping into a quick field guide:

A Practical Diagnostic Sequence

When a repository feels slow, a practical sequence looks like this:

  1. identify the command and workflow that hurt
  2. decide which Git layer that command primarily touches
  3. inspect repository layout and current accelerators
  4. trace the command if the cause is still unclear
  5. choose one targeted change
  6. rerun and compare

Worked Example: Slow git status In a Large Checkout

Suppose the complaint is:

"git status takes several seconds in this checkout."

Do not start with repack or clone flags. Start with the local evidence:

time git status >/dev/null
git config --show-origin --get-regexp '^(core\.fsmonitor|core\.untrackedCache|index\.sparse)' || true
git ls-files --debug | sed -n '1,20p'
GIT_TRACE2_PERF=/tmp/status.perf git status >/dev/null

Now read the result in order:

  1. If repeated status runs stay slow, the problem is probably real and local.
  2. If fsmonitor and untracked cache are both absent, missing local accelerators are an obvious first suspect.
  3. If the checkout is much wider than the task needs, sparse-checkout and sparse-index become the first real candidates.
  4. If Trace2 shows most of the time inside local index and working-tree regions, stop looking at packfiles and transport.

That leads to much narrower fixes: enable fsmonitor if the platform and repo support it, enable untracked cache if directory scanning is what's eating time, reduce checkout width with sparse-checkout, and consider sparse-index if the repository is large enough for index layout itself to matter. The order matters more than the menu of features, because the goal is to learn which local layer is slow and what the evidence actually says about it.

Worked Example: Slow git fetch

Now suppose the complaint is:

"Fetch is slow every time, even when the change set is small."

The first pass is different:

time git fetch --dry-run >/dev/null
git count-objects -vH
git config --show-origin --get-regexp '^(fetch\.|remote\..*promisor|maintenance\.)' || true
GIT_TRACE_PACKET=/tmp/fetch.packet GIT_TRACE2_PERF=/tmp/fetch.perf git fetch origin >/dev/null

Now the likely readings are:

  1. If packet tracing shows a long negotiation or a lot of round trips, transport and reachability are the first place to look.
  2. If Trace2 shows most local time after transfer, unpack or checkout may be part of the cost.
  3. If maintenance and bitmap-related preparation are weak on the server side, the client may be suffering from poor repository layout upstream.
  4. If the repository is large and repeatedly fetched, prefetch, bundle seeding, or partial-clone policy may change the user experience more than local checkout tuning.

That usually narrows the next step to one of: server-side maintenance and bitmap freshness, prefetch strategy, partial clone, bundle seeding for first-clone pain, or separating transfer cost from later local materialization cost.

If traces point to stale server bitmaps, pack layout, or hook cost, stop piling on client config and fix the server-side layer instead.