Troubleshooting Slow Git Operations: Common Pitfalls and Solutions
Diagnose slow Git commands by separating status, clone, fetch, push, hooks, filesystem, network, and repository-size causes.
Troubleshooting Slow Git Operations: Common Pitfalls and Solutions
Slow Git has different causes depending on which command is slow. A slow git status is usually local filesystem or index work. A slow git fetch is often network, remote size, or negotiation. A slow git checkout may be file count, antivirus scanning, sparse checkout problems, or generated files. A slow git push may be large objects, hooks, compression, or the remote server.
So the first fix is not git gc. The first fix is to measure the exact operation.
On macOS or Linux:
time git status
time git fetch --prune
time git checkout main
On PowerShell:
Measure-Command { git status }
Run the command twice. The first run may be slower because the OS cache is cold. If the first git status takes ten seconds and the second takes one, you may be looking at disk cache behavior. If both are slow, keep digging.
Git has built-in tracing that can show where time goes:
GIT_TRACE=1 git status
GIT_TRACE_PERFORMANCE=1 git status
GIT_TRACE_PACKET=1 GIT_TRACE=1 git fetch
GIT_TRACE_PACKET is noisy, but useful when fetch or push hangs during protocol negotiation. Do not paste trace output with private repository URLs or tokens into public tickets.
When git status Is Slow
git status checks the index and working tree. It gets slow when the repository has a huge number of files, the working tree is on a slow filesystem, file metadata is expensive to read, or another program scans every file Git touches.
Start with the basics:
git status --short
git config --show-origin --get core.fsmonitor
git config --show-origin --get core.untrackedCache
git config --show-origin --get core.preloadIndex
For large working trees, these settings can help on many systems:
git config core.untrackedCache true
git config core.preloadIndex true
Use local config first so you can test per repository. If it helps, make it global later.
Git's built-in filesystem monitor can speed up status by avoiding full scans on supported platforms and Git versions:
git config core.fsmonitor true
If status becomes incorrect or odd after enabling it, turn it off and update Git before trying again:
git config --unset core.fsmonitor
Untracked files can be a hidden problem. Build outputs, dependency directories, generated reports, and local logs should usually be ignored. Check what Git is scanning:
git status --untracked-files=all --short | head -100
If you see node_modules/, dist/, .venv/, target/, or similar generated directories, add the right patterns to .gitignore. Do not ignore source files just to make status faster. Ignore files that truly should not be versioned.
On Windows, real-time antivirus scanning is a common reason Git feels slow. Git reads many small files inside .git and the working tree, and security software may inspect each access. If your organization allows it, exclude trusted development workspaces from real-time scanning. Do not exclude directories where you run untrusted code.
Also avoid placing active repositories in cloud-sync folders such as OneDrive, Dropbox, or iCloud Drive. Sync tools can lock files, rewrite metadata, and compete with Git's own file operations.
When Clone or Fetch Is Slow
A slow clone can mean a large history, many large blobs, a slow remote, or a network path with high latency. Measure repository size after cloning:
git count-objects -vH
du -sh .git 2>/dev/null
For CI jobs and temporary environments, use a shallow clone when history is not needed:
git clone --depth 1 <url>
For a branch build:
git clone --depth 1 --branch main <url>
Shallow clones are not ideal for every workflow. Commands that need history, tags, merge bases, or version calculations may fail or produce incomplete answers. In CI, that is often acceptable. On a developer machine, it can be frustrating.
Partial clone is useful when the repository history is needed but file blobs can be downloaded lazily:
git clone --filter=blob:none <url>
This works best with modern Git servers that support partial clone well. Test it with your host before making it the official team recommendation.
If you only need one part of a monorepo, combine sparse checkout with a normal or partial clone:
git clone --filter=blob:none --sparse <url>
cd repo
git sparse-checkout set services/api shared/lib
Sparse checkout reduces working tree size. It does not magically make every Git operation cheap, but it helps when file count is the main problem.
For fetches with many deleted remote branches, prune stale references:
git fetch --prune
To make that the default:
git config --global fetch.prune true
When Push Is Slow
Push speed depends on how much new object data you send, how expensive local packing is, whether hooks run, and how quickly the remote accepts the pack.
Check whether you accidentally committed large files:
git rev-list --objects --all | sort -k 2 | tail
That command is crude because it does not show sizes. For deeper inspection, use tools such as git-sizer or git filter-repo analysis commands if available. The practical point is simple: if a video, database dump, archive, or build artifact entered history, every clone may pay for it until history is rewritten or the project moves to a better storage pattern.
Git LFS is the usual answer for large binary assets that belong with the project but should not live as normal Git blobs:
git lfs install
git lfs track "*.psd"
git lfs track "*.mp4"
git add .gitattributes
Git LFS helps most when adopted before large files enter history. Migrating existing history is possible, but it rewrites commits and needs team coordination.
Be cautious with old advice to raise http.postBuffer. It is often suggested for push problems, but it rarely fixes general slowness in modern Git. If pushes fail with specific HTTP errors, check the exact error, proxy, server limits, and Git version before applying random buffer settings.
Repository Maintenance: git gc, Commit Graphs, and Repack
Git stores objects in packfiles. Over time, local repositories can accumulate loose objects and inefficient packs. Git runs maintenance automatically in many workflows, but manual maintenance can still help older or busy repositories.
Start with a safe maintenance command:
git maintenance run
Or the older command:
git gc
Avoid making git gc --prune=now your casual first move. Pruning immediately removes unreachable objects that may otherwise be recoverable for a while. It can be fine when you know what you are doing, but it is not a harmless speed button.
For repositories with large histories, commit graphs can improve history walks used by commands such as log, merge-base, and fetch negotiation:
git commit-graph write --reachable
Modern Git maintenance may handle this for you. Check your version:
git --version
Keeping Git current is one of the least dramatic performance fixes. Newer versions regularly improve sparse checkout, partial clone, filesystem monitoring, and maintenance behavior.
Large Repositories and Monorepos
If a repository is slow because it is genuinely large, local tweaks only go so far. You need workflow changes.
For binary-heavy repositories, move large assets to Git LFS or an artifact store. For generated files, stop committing outputs that can be rebuilt. For monorepos, use sparse checkout and build tooling that understands project boundaries. For CI, avoid full-depth clones unless the job needs full history.
A useful monorepo setup for a developer working on one service might be:
git clone --filter=blob:none --sparse <url>
cd repo
git sparse-checkout set services/billing packages/common
A useful CI setup for a simple test job might be:
git fetch --depth 50 origin main
The right depth depends on the job. If your versioning tool uses tags from months ago, a depth of 1 will break it.
Hooks and External Tools
Git may not be the slow part. A pre-commit hook can run formatters, linters, tests, secret scans, or dependency checks. A post-checkout hook can rebuild files. A credential helper can pause while trying to unlock a keychain.
Check hooks:
git config --get core.hooksPath
ls -l .git/hooks .githooks 2>/dev/null
Temporarily compare with hooks disabled only if you understand the risk:
git commit --no-verify
For non-commit commands, move or disable the hook in a test copy of the repository rather than deleting team hooks from your main checkout.
If an IDE makes Git slow but the terminal is fast, inspect IDE Git integrations. Some tools run git status repeatedly, scan untracked files, or refresh branch state in the background.
Network and Remote Checks
For remote operations, separate Git from the network path. Try:
GIT_TRACE_PERFORMANCE=1 git ls-remote <url>
GIT_TRACE_PERFORMANCE=1 git fetch
If git ls-remote is slow, the delay happens before much repository data transfers. Think DNS, proxy, VPN, SSH authentication, remote availability, or credential prompts. If ls-remote is fast but fetch is slow, repository data size and negotiation are more likely.
For SSH remotes, test SSH directly:
ssh -T [email protected]
Use your actual Git host. For HTTPS remotes, credential manager prompts can be hidden behind GUI windows. A stalled fetch may be waiting for authentication.
A Short Decision Tree
If git status is slow, inspect untracked files, generated directories, antivirus, cloud-sync folders, filesystem monitor, and index settings.
If clone is slow, consider shallow clone, partial clone, sparse checkout, Git LFS, and whether the repository history contains large blobs.
If fetch is slow, prune stale refs, update Git, inspect network traces, and check whether the remote has many branches or tags.
If push is slow, look for large new objects, slow hooks, server-side checks, and network or proxy issues.
If all Git commands are slow, check disk health, free space, security software, Git version, and whether the repository lives on a network mount.
The best fix is the one that matches the measured bottleneck. Git performance work gets messy when every suggestion is applied at once. Change one thing, measure again, and keep the change only if it actually helps.
Team-Level Fixes Beat Personal Tweaks
If only one developer has slow Git, local settings and machine health are good places to start. If everyone has slow Git, the repository needs attention. Personal tweaks will hide the pain for a while, but new developers and CI jobs will keep paying the cost.
Look for large objects that should never have been committed, generated directories that belong in .gitignore, and old branches that keep unnecessary history alive. Before rewriting history, talk to the team. History rewrites affect every clone and every open branch. They can be worth it, but they need coordination.
For repositories with legitimate large assets, define a policy instead of relying on memory. For example: source code in Git, design exports in Git LFS, build artifacts in the artifact repository, database dumps in controlled storage, and local scratch files ignored. Put those rules in .gitattributes and .gitignore so Git can enforce the shape of the repository.
CI deserves its own review. Many pipelines clone full history because it was the default copied years ago. If the job only runs unit tests, it may not need all tags and all branches. If the job builds a release, it may need tags but not every blob in a monorepo. Measure clone time separately from build time so repository cost is visible.
A simple CI audit asks:
Does this job need full history?
Does it need tags?
Does it need every submodule?
Does it need every directory in the monorepo?
Does it fetch LFS files it never reads?
Answering those honestly often saves more time than tuning obscure Git options.
Finally, document the recommended clone command for the project. If new developers should use partial clone and sparse checkout, say so in the README. If they need Git LFS before checkout, say that too. Performance guidance that lives only in one senior developer's shell history does not help the next person.