High Performance Git

Section V ยท Diagnosis and Recovery

Chapter 19

Finding and Fixing Slow Git

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

The last chapter was about collecting evidence. This one is about using it. With a slow command, a rough baseline, and maybe one or two trace files, you can stop spraying fixes around and start narrowing the problem.


Start With the Symptom, Not the Theory

We've emphasized this consistently: the first diagnostic question is simple (even if the answer is not):

"Which command is slow, and what kind of work is that command doing?"

The command is your first clue about the layer. A tiny baseline pass already tells you a lot:

time git status
time git log -- path/to/file
time git switch main
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:

When the command crosses the network, start by timing the split:

time git fetch --dry-run
time git fetch

--dry-run does not reproduce every cost, 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

That is enough to get out of guesswork and into evidence quickly.

git status Problems Usually Mean Local Filesystem and Index Cost

If the complaint is status, the likely cost layers are:

That points you toward the index, sparse-index, untracked cache, fsmonitor, split index, and worktree reduction.

Ask:

A slow status is usually not a graph problem.

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:

That points you toward commit-graph, changed-path Bloom filters, and the history-traversal chapter.

Ask:

A path-limited history query shows why command form matters. It is a graph walk with path reasoning and, in some cases, rename heuristics layered on top.

The corresponding probe is equally direct:

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:

That points you toward worktrees, sparse-checkout, sparse-index, and the earlier command-semantics material.

Ask:

Slow checkout often means too much local materialization, not bad history storage.

Start with the simplest visible command:

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:

That points you toward protocol v2, bundle URIs, bitmaps, and partial clone.

Ask:

Network diagnosis needs its own layer. Many local optimizations do not help a bad clone path.

The first transport pass should usually include:

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 subsystem is known, the fix choices are narrow.

Working-tree and index fixes:

History and graph fixes:

Storage fixes:

Transport fixes:

You can compress that mapping into a quick field guide:

Avoid Over-Fixing the Wrong Layer

A common failure mode in Git tuning is technically improving one layer while the visible problem lives somewhere else.

Examples:

The symptom-to-subsystem mapping matters because it protects you from paying for fixes that are real but irrelevant.

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:

The order matters more than the menu of features: which local layer is slow, and what does the evidence say 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 paying for 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:

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