High Performance Git

Section V ยท Write Pressure, Diagnosis, and Recovery

Chapter 21

Configuration Playbook

Pencil sketch of a waterfront gazebo with a small band performing and townspeople gathered nearby.

After a few tuning passes, a large repository tends to accumulate the same config lines:

The hard part is not the names. It is knowing which ones affect local scans, which ones change fetch policy, and which ones only matter if background maintenance is actually running.


Read the Current State Before You Tune It

Most configuration mistakes are not wrong values. They are values whose origin nobody remembers.

Start by reading the current posture with origin information attached:

git config --show-origin --get-regexp '^(core\.fsmonitor|core\.untrackedCache|core\.splitIndex|core\.commitGraph|feature\.manyFiles|index\.sparse|maintenance\.strategy|gc\.writeCommitGraph|fetch\.writeCommitGraph|status\.aheadBehind|protocol\.version|fetch\.negotiationAlgorithm|(transfer|fetch|receive)\.fsckObjects|remote\..*\.(promisor|partialclonefilter)|extensions\.worktreeConfig|safe\.directory)$' || true

Git config has layers:

If a setting is coming from a global profile, a template directory, or a worktree-local file, that changes how you reason about the next command.

Confirm the Setting Did Something

The minimum loop is two traces, one before and one after the change:

GIT_TRACE2_PERF=/tmp/before.perf git status
git config core.fsmonitor true
GIT_TRACE2_PERF=/tmp/after.perf git status

The region that should move depends on the setting. Tracked-path refresh time matters for core.fsmonitor and core.untrackedCache. Index write time matters for core.splitIndex. Negotiation time matters for fetch.negotiationAlgorithm.

If the two traces look the same, the setting is not affecting the path you care about. The cost lives somewhere else, and the next setting is unlikely to help either until you find it.

core.fsmonitor Cuts Tracked-Path Refresh Cost

Fsmonitor
Integration that lets Git learn which paths changed from a filesystem monitor instead of probing every tracked path itself.

core.fsmonitor=true tells Git to ask a filesystem monitor which tracked paths changed instead of probing the whole tracked set with ordinary stat calls.

git config --get core.fsmonitor
git config core.fsmonitor true
git config --unset core.fsmonitor

Reach for it when repeated status, add, and branch switches are spending too much time on tracked-path refresh across a large checkout. Skip it on platforms where the built-in monitor is unreliable, on remote mounts the daemon refuses to watch, or in mixed-toolchain checkouts that still see Git 2.35.1 or older. Git 2.35.1 and older misread boolean core.fsmonitor values as hook pathnames. If an IDE, wrapper, or older system Git still touches the checkout, core.fsmonitor=true can break things.

core.untrackedCache Avoids Re-Scanning Quiet Directories

Untracked Cache
Index extension that caches directory mtimes to avoid re-scanning unchanged directories when looking for untracked files.

core.untrackedCache=true tells Git to cache directory mtimes so it can avoid repeating full untracked scans in directories that did not change.

git config --get core.untrackedCache
git config core.untrackedCache true
git config core.untrackedCache false

Use it when broad working directories, build outputs, or generated trees dominate status. It does little for tracked-path refresh. Check that mtime behaves properly on the system before enabling it. If directory mtimes are unreliable because of the filesystem or surrounding tools, the cache is not worth it.

core.splitIndex Shrinks Repeated Index Rewrites

Split Index
Mode that stores a stable shared index plus a smaller mutable overlay to reduce repeated full-index rewrites.

core.splitIndex=true makes Git keep a stable shared index plus a smaller mutable overlay. It helps when the index is large and most commands change only a small part of it.

git config --get core.splitIndex
git config core.splitIndex true
git config core.splitIndex false

Split index cuts index rewrite cost only โ€” not untracked discovery, history walks, or transport.

feature.manyFiles Is a Bundle, Not a Neutral Toggle

feature.manyFiles=true is one of the few high-level convenience knobs worth discussing directly because it enables several lower-level settings together.

git config --get feature.manyFiles
git config feature.manyFiles true
git config --unset feature.manyFiles

It implies:

That bundle helps on very large working trees, but it is not neutral. index.skipHash makes Git clients older than 2.13.0 refuse to parse the index, and clients older than 2.40.0 report an error during git fsck.

index.sparse Makes the Index Match a Narrow Working Tree

index.sparse=true lets Git write sparse-directory entries into the index so the index shape better matches a sparse working tree.

git config --get index.sparse
git sparse-checkout set --sparse-index src docs
git sparse-checkout reapply --no-sparse-index

index.sparse has no effect unless core.sparseCheckout and core.sparseCheckoutCone are both enabled. In practice, enable it through git sparse-checkout set --sparse-index instead of toggling the raw config by hand.

Only enable it once sparse-checkout is active. If the working tree is still dense, skip it. If outside tools choke on sparse-directory entries, turn it back off. Current docs are blunt here: older Git versions may fail to interact with the repository until the sparse index is disabled again.

maintenance.strategy Is About Schedule, Not Magic

maintenance.strategy picks a recommended background schedule for git maintenance run --schedule=<frequency>. Setting it alone does nothing unless the schedule actually runs.

git config --get maintenance.strategy
git maintenance start
git maintenance unregister --force

The two strategy strings are none and incremental. For large repositories, incremental is the one that usually matters. It schedules:

git maintenance start is the operating model, not a speed flag โ€” it wires the scheduler into the host. Without an actual scheduler, maintenance.strategy is decorative.

fetch.writeCommitGraph Decides Where Graph Refresh Cost Lives

fetch.writeCommitGraph=true tells Git to write a commit-graph after fetches that download a pack. That helps later graph-heavy commands, but it also puts more work on the foreground fetch path.

git config --get fetch.writeCommitGraph
git config fetch.writeCommitGraph false
git config --unset fetch.writeCommitGraph

Leave it true when fetches are infrequent enough that doing the graph update in the foreground is acceptable. Set it false when fetch latency shows up in daily work, background maintenance is already handling commit-graph updates, or the repository is large enough that you want fetch out of the foreground path quickly.

core.commitGraph and gc.writeCommitGraph Keep the Graph File Read and Refreshed

core.commitGraph=true tells Git to read the commit-graph file when answering history-walk questions. gc.writeCommitGraph=true tells git gc and git maintenance run to refresh that file as part of upkeep.

git config --get core.commitGraph
git config --get gc.writeCommitGraph
git config core.commitGraph true
git config gc.writeCommitGraph true

core.commitGraph defaults to true, but verify it. Something upstream in the config chain can disable it, and a missing read defeats the reason to write the file at all.

gc.writeCommitGraph is the maintenance-side counterpart to fetch.writeCommitGraph. Fetch decides whether the foreground path pays for graph refresh. GC and maintenance decide whether routine upkeep keeps the file current. If background maintenance is already running, leave gc.writeCommitGraph=true and let fetch.writeCommitGraph=false keep fetches lean.

If neither path writes the file, the commit-graph drifts and graph-heavy commands quietly fall back to walking raw commit objects.

status.aheadBehind Is Often Worth Turning Off in Large Repositories

status.aheadBehind=false removes the ahead/behind calculation from ordinary git status output.

git config --get status.aheadBehind
git config status.aheadBehind false
git config --unset status.aheadBehind

This one is easy. If branch divergence calculations are making everyday status feel heavier than they should, turn it off. If the repository is small and the ahead/behind count is cheap and useful, keep it.

protocol.version and fetch.negotiationAlgorithm Shape Fetch Behavior

protocol.version and fetch.negotiationAlgorithm sit on the transport side rather than the local working-tree side.

git config --get protocol.version
git config --get fetch.negotiationAlgorithm
git config protocol.version 2
git config fetch.negotiationAlgorithm skipping

protocol.version already defaults to 2 when unset. Setting it explicitly mostly makes the policy explicit.

fetch.negotiationAlgorithm=skipping is the sharper knob. It reduces negotiation round trips by skipping commits more aggressively, but it can also produce a larger-than-necessary packfile:

Use it only when negotiation is the bottleneck. If the network path is fine or transfer size matters more than negotiation latency, leave it alone.

remote.<name>.promisor and remote.<name>.partialclonefilter Change Fetch Policy

remote.<name>.promisor=true and remote.<name>.partialclonefilter=blob:none change what object absence means in the repository.

git config --get-regexp '^remote\.origin\.(promisor|partialclonefilter)$'
git config remote.origin.promisor true
git config remote.origin.partialclonefilter blob:none
git fetch --refetch origin

Use them when up-front blob transfer is what hurts, later on-demand hydration is acceptable, and the team understands that some objects will arrive later.

Changing remote.<name>.partialclonefilter only affects fetches for new commits. The --refetch form helps align later fetch policy. An existing full clone stays full unless you reclone.

transfer.fsckObjects Trades Throughput for Integrity Checks

transfer.fsckObjects=true tells Git to validate object integrity on both fetch and receive. fetch.fsckObjects and receive.fsckObjects set the same policy on one direction at a time, and either one overrides transfer.fsckObjects.

git config --get-regexp '^(transfer|fetch|receive)\.fsckObjects$'
git config receive.fsckObjects true
git config fetch.fsckObjects true

fsck adds CPU cost to transfers in exchange for catching malformed or malicious objects before they land in the object database. Reach for receive.fsckObjects=true on shared servers, especially when pushes come from many clients. Reach for fetch.fsckObjects=true on long-lived clones where corruption would be expensive to discover later. Skip it on hot-path mirroring or large CI fleets where the throughput cost shows up and another layer is already validating.

extensions.worktreeConfig Keeps Local Tuning Local

extensions.worktreeConfig=true lets Git read and write a worktree-specific config file so local settings stay attached to one worktree instead of leaking into the whole repository.

git config --get extensions.worktreeConfig
git config extensions.worktreeConfig true
git config --worktree core.sparseCheckout true
git config --worktree index.sparse true

Use it when the repository has several linked worktrees with different local roles:

It is not a speed feature on its own โ€” it isolates per-worktree tuning so local settings don't bleed across unrelated worktrees.

safe.directory Is an Ownership Check, Not a Performance Knob

safe.directory controls which directories Git is willing to operate inside when the on-disk owner does not match the calling user. It is not a speed setting, but it lands in performance playbooks anyway because it blocks Git silently in CI and shared-host environments.

git config --global --get-all safe.directory
git config --global --add safe.directory /srv/repos/example
git config --global --add safe.directory '*'

Git started enforcing this ownership check after CVE-2022-24765. A repository owned by one user but operated on by another now needs a matching safe.directory entry, otherwise Git refuses to read or write it. The wildcard form (safe.directory=*) disables the check entirely. That is reasonable inside a container or single-tenant CI image. It is not reasonable on a multi-user host, where listing specific paths is the better answer.

Prefer Bundles With a Reason

The best configuration posture is usually a small coherent bundle. Twelve unrelated lines copied from three blog posts is a good way to make the repository harder to reason about later. A large local checkout bundle usually looks like:

git config core.fsmonitor true
git config core.splitIndex true
git config core.untrackedCache true

# Only when every Git touching the checkout is recent enough:
git config feature.manyFiles true

A background-maintenance bundle usually looks like:

git maintenance start
git config fetch.writeCommitGraph false
git config status.aheadBehind false

A blobless transfer bundle usually looks like:

git config protocol.version 2
git config fetch.negotiationAlgorithm skipping
git config remote.origin.promisor true
git config remote.origin.partialclonefilter blob:none

Each of those bundles answers one question:

As always: start with the cost you are trying to reduce, then choose the settings that actually affect it.