Git Maintenance: Keeping Repositories Fast
Git repositories accumulate: loose objects from every commit, refs from every branch, reflog entries from every operation. Left alone, a busy repository gets slower.
Git handles most of this automatically. git maintenance gives you control over when and which tasks
run, which matters on large repositories where the automatic behaviour is either too infrequent or badly
timed.
The single most useful thing to know is when not to intervene: for most repositories, Git’s defaults are correct and manual optimisation is at best a waste of time.
What accumulates
Section titled “What accumulates”Loose objects. Every git add and git commit writes new objects as individual compressed files. A
day’s work can produce thousands.
Packfiles. Repacking consolidates loose objects, but repeated repacks leave several packfiles, and Git must search each one.
Unreachable objects. Amended commits, deleted branches, abandoned rebases. They remain until pruned.
Refs. Thousands of branches and tags as individual files makes any ref-scanning operation slow.
Reflog entries. Every ref movement, retained for 90 days by default.
None of this is a problem at small scale. On a repository with a million commits and tens of thousands of refs, each of them costs measurable time on ordinary commands.
Automatic maintenance
Section titled “Automatic maintenance”Git already runs maintenance for you. Commands that create objects — commit, merge, rebase, am —
occasionally trigger git gc --auto, which does work only if thresholds are exceeded:
| Setting | Default | Triggers |
|---|---|---|
gc.auto | 6700 | Loose objects before repacking |
gc.autoPackLimit | 50 | Packfiles before consolidating |
gc.autoDetach | true | Run in the background so you are not blocked |
git count-objects -vcount: 0size: 0in-pack: 11packs: 2size-pack: 3prune-packable: 0garbage: 0size-garbage: 0count is loose objects; in-pack is packed ones. If count is in the low thousands, automatic
maintenance has not run recently and does not need to.
git gc
Section titled “git gc”The traditional command. It repacks loose objects, consolidates packfiles, packs refs, expires reflog entries and prunes unreachable objects.
git gcgit count-objects -v | grep -E 'count|in-pack'count: 0in-pack: 11Loose objects have moved into a packfile. Refs are consolidated too:
cat .git/packed-refs# pack-refs with: peeled fully-peeled sorted4ff2767a422691863b00b07ee6e51de7a65b1919 refs/heads/maingit gc --aggressive recomputes delta compression from scratch. It is slow — hours on a large repository —
and the benefit is usually small. Reach for it at most once after an unusual event such as a large import,
never routinely.
git maintenance
Section titled “git maintenance”The modern interface. Rather than one monolithic gc, it exposes individual tasks that can be run or
scheduled separately.
git maintenance run --task=commit-graphWhat it doesRuns one named maintenance task immediately in the current repository.
Why we run itIndividual tasks are far cheaper than a full gc, so they can run frequently without disruption.
Expected resultUsually no output. Effects are visible in .git — for example a new file under objects/info/.
ls .git/objects/info/commit-graphsThe available tasks:
| Task | Does | Cost |
|---|---|---|
commit-graph | Builds a cache of commit metadata and ancestry | Low |
prefetch | Fetches from remotes in the background | Low, network |
loose-objects | Packs a batch of loose objects | Low |
incremental-repack | Consolidates packfiles gradually | Moderate |
pack-refs | Packs refs into a single file | Low |
gc | The full traditional collection | High |
An invalid name lists nothing helpful, so keep this table handy:
git maintenance run --task=nopeerror: 'nope' is not a valid taskcommit-graph
Section titled “commit-graph”The highest-value task for most large repositories. It caches each commit’s parents, generation numbers and other metadata in a single file, so operations that walk history do not have to read and decompress every commit object.
git log --graph, git merge-base, git branch --contains and anything computing reachability all get
substantially faster. On a repository with hundreds of thousands of commits the difference is dramatic.
prefetch
Section titled “prefetch”Fetches from remotes in the background into a private ref namespace, without touching your
remote-tracking branches. When you later run git fetch, most of the objects are already local, so it
completes quickly.
Because it does not update origin/*, it never changes what your commands report — it only makes the
eventual fetch cheap.
incremental-repack
Section titled “incremental-repack”Consolidates packfiles gradually rather than rewriting everything at once. This is the key difference from
gc: it bounds the work per run, so it can be scheduled hourly without ever blocking you for minutes.
Scheduling
Section titled “Scheduling”git maintenance start registers the repository and installs a schedule using the platform’s own
mechanism — systemd timers, launchd, or Task Scheduler depending on the operating system.
git maintenance startThereafter Git runs the appropriate tasks on an hourly, daily and weekly cadence without you doing anything.
To register a repository without installing the scheduler — useful when you manage scheduling yourself:
git maintenance registergit config --global --get-all maintenance.repo/home/you/projectAnd to undo either:
git maintenance unregistergit maintenance stopWhen to intervene
Section titled “When to intervene”Do intervene when:
- Everyday commands have become noticeably slow in a large repository.
git count-objects -vshows tens of thousands of loose objects or dozens of packfiles.- You have just imported a large history or completed a major rewrite.
- You are on a very large repository and want
commit-graphandprefetchscheduled. - You need to reclaim disk after deliberately removing large objects from history.
Do not intervene when:
- Git feels fast. There is nothing to fix.
- You read that
--aggressivemakes things faster. Usually it does not, and it is expensive. - You want to “clean up” a repository. Automatic maintenance already does this.
- You are tempted by
--prune=nowfor tidiness. It destroys recoverable work.
Large repositories: what actually helps
Section titled “Large repositories: what actually helps”The features that make a very large repository usable are mostly not gc. In rough order of impact:
1. The commit-graph. Turns history traversal from “read and decompress every commit object” into a lookup in a purpose-built file. Enable it and keep it current:
git config --global fetch.writeCommitGraph truegit maintenance run --task=commit-graph2. A filesystem monitor. git status on a huge working tree spends its time asking the operating
system about files. core.fsmonitor makes Git subscribe to change notifications instead:
git config core.fsmonitor trueGit includes a built-in monitor daemon on supported platforms. This is the single biggest improvement for
git status on a working tree with hundreds of thousands of files.
3. The untracked cache, which avoids re-scanning directories for untracked files:
git config core.untrackedCache true4. A sparse index, if you also use sparse checkout — it shrinks the index itself rather than only the working tree. See Sparse Checkout.
5. Scheduled incremental maintenance, so repacking happens gradually rather than as an occasional long pause.
Diagnosing slowness
Section titled “Diagnosing slowness”Slowness usually has a specific cause, and maintenance only fixes some of them.
| Symptom | Likely cause | Fix |
|---|---|---|
git status is slow | Very large working tree | Sparse checkout with --sparse-index, or core.fsmonitor |
git log --graph is slow | No commit-graph | git maintenance run --task=commit-graph |
git branch is slow | Thousands of loose refs | git maintenance run --task=pack-refs |
git fetch is slow | Large transfers | prefetch task, or partial clone |
| Everything is slow | Many loose objects or packfiles | git gc, or incremental-repack |
| Clone is slow | Repository size | Partial or shallow clone |
Note that the first row is not a maintenance problem at all. A slow git status on a huge working tree is
about the number of files on disk, and no amount of repacking will help.
What each task actually does
Section titled “What each task actually does”Knowing what a task changes on disk makes it much easier to tell whether it helped.
commit-graph writes .git/objects/info/commit-graphs/. It stores each commit’s parents, root tree,
commit date and a generation number — a precomputed value that lets Git answer “is A an ancestor of B?”
without walking the graph. That single optimisation is why history-traversal commands speed up so
dramatically on large repositories.
pack-refs consolidates .git/refs/** into .git/packed-refs. After it runs, listing files under
refs/heads/ may show nothing while the branches all still exist — Git checks both locations. This is why
git show-ref is the correct way to enumerate refs.
loose-objects takes a bounded batch of loose objects and packs them. Because the batch is bounded,
it never blocks you for long, unlike gc.
incremental-repack combines small packfiles into larger ones a few at a time, converging on a good
layout without ever rewriting everything at once.
prefetch fetches into refs/prefetch/, a private namespace. Your origin/* refs are untouched, so
nothing you see changes — but the objects are already local when you next fetch.
gc does all of the above plus pruning, in one potentially long operation.
Disk usage
Section titled “Disk usage”Repacking reduces size, but there are limits worth understanding before you spend an afternoon on it.
du -sh .gitgit count-objects -vH-H prints human-readable sizes. If size-pack is large, the objects themselves are large — repacking
will not change that materially.
The usual causes of a large .git:
| Cause | Fix |
|---|---|
| Many loose objects | git gc — genuinely helps |
| Large binaries in history | Rewriting history, or Git LFS going forward |
| Long history of a large codebase | Partial or shallow clone |
| Unreachable objects from rewrites | gc after the reflog expires |
Reflog expiry
Section titled “Reflog expiry”Maintenance also expires reflog entries, which is worth understanding because it determines your recovery window:
| Setting | Default | Applies to |
|---|---|---|
gc.reflogExpire | 90 days | Entries for reachable commits |
gc.reflogExpireUnreachable | 30 days | Entries for unreachable commits |
Those defaults are generous, and lengthening them is rarely necessary. Shortening them narrows the window in which recovery from a bad rewrite is possible, which is a poor trade for a small amount of disk.
Common mistakes
Section titled “Common mistakes”Running git gc --aggressive routinely. Expensive, and usually achieves nothing measurable.
Using --prune=now as a cleanup habit. Destroys objects the reflog could still recover.
Optimising a repository that is not slow.
Expecting maintenance to fix a slow working tree. That is a file-count problem.
Deleting .git/objects contents by hand. Never do this. Use Git’s commands.
Assuming git gc shrinks a repository containing large files in history. Repacking helps a little;
the objects are still there. Removing them requires rewriting history, with all the consequences that
entails.
Enabling scheduled maintenance on a machine where background jobs are unwelcome.
Mental Model
Section titled “Mental Model”Git maintenance is tidying a workshop.
Offcuts accumulate as you work. Git sweeps up automatically when there is enough to be worth sweeping, and for most workshops that is sufficient.
git maintenancelets you schedule the sweeping for a convenient time and choose which jobs to do — which matters in a very large workshop where sweeping everything at once would stop work for an hour.
--prune=nowis emptying the bins before checking whether you threw something away by mistake.
What You Learned
Section titled “What You Learned”- Git runs maintenance automatically via
gc --auto, triggered by thresholds on loose objects and packs. git count-objects -vshows whether anything needs doing.git maintenance run --task=<name>runs individual tasks:commit-graph,prefetch,loose-objects,incremental-repack,pack-refs,gc.commit-graphis the highest-value task on large repositories.incremental-repackbounds the work per run, unlikegc.git maintenance startinstalls a platform-native schedule;registerrecords the repository only.--prune=nowdestroys unreachable objects the reflog might still recover.--aggressiveis slow and rarely worth it.- Most repositories need no manual maintenance at all.
Try It Yourself
Section titled “Try It Yourself”-
Create a repository with some churn:
Terminal window mkdir ~/maint-lab && cd ~/maint-lab && git initfor i in $(seq 1 30); do echo "line $i" >> f.txt; git add . && git commit -qm "commit $i"; done -
Look at the object counts:
git count-objects -v. Notecountandin-pack. -
Check what is in the objects directory:
find .git/objects -type f | wc -l. -
Build a commit-graph:
Terminal window git maintenance run --task=commit-graphls .git/objects/info/ -
Pack the refs:
Terminal window git maintenance run --task=pack-refscat .git/packed-refsls .git/refs/heads/ 2>/dev/nullPredict: will
refs/heads/still contain a file formain? -
Run a full gc and compare:
Terminal window git gcgit count-objects -v -
Confirm nothing was lost:
git log --oneline | wc -lshould still be 30. -
Register and unregister, without installing a scheduler:
Terminal window git maintenance registergit config --global --get-all maintenance.repogit maintenance unregister
Step 5 demonstrates why listing .git/refs/heads/ is an unreliable way to find branches — after packing,
the files are gone and the branches are not. Use git show-ref or git branch.
Next Lesson
Section titled “Next Lesson”Maintenance settings, hook paths, aliases and everything else live in Git configuration. The next lesson is the full reference.