Scheduling git maintenance for background repacking Jump to heading

Every commit, fetch and rebase writes loose objects. Git’s historical answer was to notice the pile-up during an ordinary command and pack it right there β€” the familiar Auto packing the repository for optimum performance message that arrives, without fail, in the middle of the one command you needed to finish quickly. The modern answer is a scheduler that does the same work on its own time. This recipe applies the maintenance half of Large Repository Performance, and it is the cheapest improvement on that page: two commands, no workflow change, no coordination with anyone.

When to use this approach Jump to heading

  • Developers occasionally report a Git command hanging for tens of seconds with an β€œauto packing” message.
  • git count-objects -vH shows tens of thousands of loose objects on a working clone.
  • git log, git blame or a review tool’s branch comparison feels slow relative to the repository’s size β€” the commit-graph is probably missing or stale.
  • Fetches are slower than the amount of new work justifies, and a background prefetch would absorb the cost.
  • The repository is otherwise healthy: if the problem is genuinely a multi-gigabyte history, pair this with partial clone rather than expecting maintenance alone to fix it.

Step 1 β€” Confirm the symptom before changing anything Jump to heading

# Loose objects are the ones that have not been packed yet
git count-objects -vH
# Watch for: count: 84213   size: 612.40 MiB   (a large "count" is the tell)

# Is a commit-graph present, and how old is it?
ls -l .git/objects/info/commit-graph* 2>/dev/null || echo "no commit-graph"

# Baseline the operations you expect to improve
time git log --oneline -5000 > /dev/null
time git status

What changed: nothing β€” but you now have a loose-object count and two timings to compare against.

What packing and the commit-graph actually changeBefore maintenance, thousands of individual loose object files must each be opened to answer a history query. After packing, they live in one pack file with an index, and the commit-graph precomputes ancestry so queries answer without walking objects at all.before β€” loose objects84 213 individual filesone filesystem open per object lookupone pack file+ indexbinary-search lookupcommit-graph β€” ancestry, precomputedwithout it: walk every commit object to answer"is A an ancestor of B?"with it: read generation numbers and stop earlythis is why a small repository can still feel slow β€” and why size is the wrong thing to measure

Step 2 β€” Register the repository with the scheduler Jump to heading

# Register this repository and install a platform scheduler entry
git maintenance start

What changed: Git added the repository to a global list and created a scheduler entry β€” a systemd timer on Linux, a launchd agent on macOS, a scheduled task on Windows β€” that runs git maintenance run on an hourly, daily and weekly cadence.

# Which repositories are registered?
git config --global --get-all maintenance.repo

# What is the schedule?
systemctl --user list-timers 'git-maintenance*'    # Linux
# launchctl list | grep git-maintenance            # macOS

Step 3 β€” Disable the blocking automatic collector Jump to heading

Registering the scheduler does not switch the old behaviour off. Both will run until you say otherwise, which means you keep the interruption you were trying to remove.

# Stop the in-command collector from firing
git config maintenance.auto false

# Belt and braces on older tooling that still consults gc.auto
git config gc.auto 0

What changed: packing now happens only on the schedule, never as a surprise inside git commit or git checkout.

git config maintenance.auto     # expect: false
git config gc.auto              # expect: 0

SAFETY WARNING β€” with automatic collection disabled, nothing reclaims space on a machine where the scheduler never runs: a container image, a CI runner, or a server where the user session that owns the timer does not persist. On those hosts either leave maintenance.auto alone or add an explicit git maintenance run --task=gc to your provisioning. A repository that grows unbounded because both mechanisms are off is a worse outcome than the interruption.

Step 4 β€” Tune which tasks run and how often Jump to heading

The maintenance tasks and their cadencePrefetch runs hourly and downloads new objects into a hidden namespace. Loose-objects and incremental-repack run daily and consolidate storage. Commit-graph runs daily and keeps ancestry queries fast. Pack-refs runs weekly and compacts the ref store. Gc is off by default because the other tasks replace it.taskcadencewhat it doesprefetchhourlydownloads new objects into a hidden ref namespaceloose-objectsdailymoves loose objects into a pack, in batchesincremental-repackdailyconsolidates small packs without a full rewritecommit-graphdailyrewrites the ancestry index β€” the biggest log speeduppack-refsweeklycompacts the ref store β€” matters above ~10 000 refsgcoffrun by hand when you need to reclaim space deliberately
# Turn an individual task on or off
git config maintenance.prefetch.enabled true
git config maintenance.commit-graph.enabled true
git config maintenance.pack-refs.enabled false   # not worth it below ~10 000 refs

# Run one task immediately, without waiting for the schedule
git maintenance run --task=commit-graph
# Confirm the graph was written and history queries improved
ls -l .git/objects/info/commit-graph*
time git log --oneline -5000 > /dev/null    # compare against the Step 1 baseline

The prefetch task deserves a note because its behaviour surprises people who expect it to update their branches. It downloads new objects into a hidden namespace and deliberately leaves remote-tracking refs alone, so nothing moves under your feet β€” but the next git fetch you run is nearly instant because the data is already on disk.

Step 5 β€” Verify the schedule is actually firing Jump to heading

Three checks that the schedule is realFirst confirm the repository is registered in the global config. Then confirm the platform scheduler has an entry with a recent last-run time. Finally confirm the loose-object count is lower than it was, which is the outcome the whole arrangement exists to produce.1 Β· registered?git config --global--get-all maintenance.repoyour path must appear2 Β· scheduled?systemctl --userlist-timers 'git-maint*'check LAST, not just NEXT3 Β· working?git count-objects -vHcount falls day over dayno "auto packing" messagesa timer that exists but has never run is the most common failure β€” check the LAST columnif step 2 shows no last run, the user session that owns the timer is not persisting between logins
# 1. Registered
git config --global --get-all maintenance.repo | grep "$(pwd)"

# 2. Scheduled, and has actually run
systemctl --user list-timers 'git-maintenance*'
#    LAST must be populated, not "n/a"

# 3. Producing the intended effect, checked a day later
git count-objects -vH    # "count:" should be far lower than the Step 1 figure

If the timer exists but has never fired, the usual cause on Linux is that the user session does not linger after logout. loginctl enable-linger "$USER" fixes it, and is worth checking before concluding that maintenance does not work.

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Is git maintenance a replacement for git gc? Jump to heading

For everyday upkeep, yes β€” it runs the same underlying work as scheduled background tasks instead of as an interruption in the middle of someone’s command. It is deliberately more conservative: incremental repack rather than a full repack, and no aggressive pruning of unreachable objects. When you genuinely need to reclaim space after deleting large objects, run git gc --prune deliberately; do not wait for maintenance to do it.

Does the prefetch task change what I see in git branch? Jump to heading

No, and that is the point. Prefetch downloads new objects into a separate hidden ref namespace rather than updating your remote-tracking branches, so nothing appears to change until you run git fetch yourself. The next fetch is then nearly instant because the objects are already local β€” you get the speed without the surprise of refs moving under you.

Will this run on a laptop that is asleep? Jump to heading

The task simply does not fire while the machine is asleep and catches up on the next scheduled window. Because the tasks are incremental, a few missed runs are harmless β€” the loose-object count grows a little and is packed at the next opportunity. This is exactly why the incremental design matters more than the frequency.