How to Speed Up brew update (And Why It's Slow)
brew update pulls a 700MB+ git repository every time. Here are workarounds — and why stout's 3MB SQLite index makes the problem disappear.
brew update is the command most Homebrew users dread. It runs automatically before installs, it takes anywhere from 10 to 60 seconds, and it often feels like it does nothing useful. But understanding what it actually does — and why — reveals both practical workarounds and the deeper architectural problem.
What brew update actually does
When you run brew update, Homebrew performs these operations:
-
Git fetch on homebrew-core. Homebrew pulls the latest commits from
https://github.com/Homebrew/homebrew-core. This is a git repository containing Ruby formula files for all 7,000+ packages. The.gitdirectory alone is roughly 700MB. -
Git fetch on homebrew-cask. If you have casks installed, it also pulls
homebrew-cask, another large git repository with ~5,000 cask definitions. -
Git fetch on any taps. Each additional tap (e.g.,
homebrew/services, third-party taps) gets its own git fetch. -
Merge or rebase. After fetching, Homebrew merges or rebases your local branch to incorporate upstream changes.
-
Migration checks. Homebrew runs migration scripts to handle breaking changes between versions.
-
JSON API cache update. In newer versions, Homebrew also updates its local JSON API cache, which involves downloading and parsing a multi-megabyte JSON file.
You can see the full breakdown with verbose output:
brew update --verbose
Watch for lines like:
Fetching /opt/homebrew/Library/Taps/homebrew/homebrew-core...
remote: Enumerating objects: 1847, done.
remote: Counting objects: 100% (1233/1233), done.
remote: Compressing objects: 100% (42/42), done.
Why it’s slow: the numbers
The homebrew-core repository has over 700MB of git objects as of 2026. Even an incremental git fetch after one day involves:
- Network transfer: 2-15MB of new git objects (delta-compressed)
- Git operations: Object decompression, index update, working tree checkout — this is CPU and I/O intensive
- Multiple repositories: With homebrew-core + homebrew-cask + any taps, you may be running 3-5 git fetches sequentially
On a fast network, the bottleneck is usually git’s CPU-bound operations (decompressing objects, updating the index). On a slow network or high-latency connection, the bottleneck shifts to the transfer itself.
Here’s a typical breakdown on an M2 MacBook Pro with a 100Mbps connection:
| Phase | Time |
|---|---|
| Git fetch homebrew-core | 5-20s |
| Git fetch homebrew-cask | 3-10s |
| Merge/rebase | 1-3s |
| Migration checks | 0.5-1s |
| JSON API cache | 2-5s |
| Total | 12-40s |
And this happens every time. If you haven’t updated in a week, the git fetch takes even longer because there are more objects to transfer and more commits to process.
Workaround 1: Disable auto-update
The most impactful single change is preventing brew update from running automatically:
export HOMEBREW_NO_AUTO_UPDATE=1
Add this to your ~/.zshrc or ~/.bashrc. Then run brew update manually when you actually want to update — perhaps once a week or before important installs.
The downside: you may install packages against stale metadata. If a security vulnerability was patched yesterday, you won’t get the fix until you manually update.
Workaround 2: Reduce update frequency
Instead of disabling auto-update entirely, you can increase the interval:
export HOMEBREW_AUTO_UPDATE_SECS=86400 # Once per day (default: 300)
The default is 300 seconds (5 minutes), which means Homebrew re-checks on nearly every invocation. Setting it to 86400 (24 hours) or even 604800 (7 days) dramatically reduces how often you hit the slow path.
Workaround 3: Use the API instead of git
Newer versions of Homebrew can use the JSON API instead of cloning git repositories:
export HOMEBREW_INSTALL_FROM_API=1
This is now the default in recent Homebrew versions and avoids cloning the full homebrew-core repo. However, it replaces the git overhead with JSON API overhead — you’re trading one slow operation for a somewhat less slow one. API updates still involve downloading and parsing a multi-megabyte JSON file.
Workaround 4: Shallow clone the tap
If you need the git-based tap for some reason, you can convert it to a shallow clone:
cd "$(brew --repository homebrew/homebrew-core)"
git fetch --depth=1
git gc --prune=all
This reduces the .git directory from 700MB+ to roughly 50-100MB. The tradeoff is that certain operations (like brew log or viewing formula history) won’t work, and the next full fetch may re-download everything.
Workaround 5: Run updates in the background
You can trigger brew update in a background subshell so it doesn’t block your interactive session:
# Add to .zshrc
(brew update &>/dev/null &)
This kicks off the update when your shell starts, so by the time you actually run brew install, the update has likely already completed. The downside is it uses bandwidth and CPU in the background, and there’s a race condition if you install something before the update finishes.
Why workarounds aren’t enough
All of these workarounds share a common problem: they’re mitigating a fundamentally slow operation rather than eliminating it. The core issue is that Homebrew’s metadata distribution is built on git, which was designed for source code version control — not for distributing a package index.
Git is great at tracking line-by-line changes to text files over time. It’s terrible at serving as a package metadata database. Every git fetch must:
- Negotiate objects with the remote (round trips)
- Download delta-compressed object packs
- Decompress and index those objects locally
- Update refs and the working tree
None of this is necessary for a package manager. You don’t need the history of every formula change since 2009. You need the current state of the package index.
How stout eliminates the problem
stout replaces the entire git-based metadata system with a pre-computed SQLite database:
- Size: ~3MB compressed with zstd (vs 700MB+ git repository)
- Update time: 1-3 seconds to download the full index (vs 10-60 seconds for git fetch)
- Content: FTS5 full-text search tables, pre-resolved dependency graphs, bottle URLs, and version metadata for all 170,000+ formulae
- Freshness check: A lightweight HTTP HEAD request determines if the index is stale — if it’s current, there’s zero work to do
time stout update
# Checking index freshness... current (rebuilt 47 minutes ago)
# real 0m0.1s
When the index is actually stale:
time stout update
# Downloading index... 3.1 MB
# Index updated: 172,431 formulae, 7,892 casks
# real 0m1.4s
The SQLite index is rebuilt on Neul Labs’ CI infrastructure by transforming the Homebrew API data into an optimized database. This means the expensive work — parsing all 170,000+ formulae, resolving dependency graphs, building search indices — happens once on a server, not on every developer’s machine.
There’s no git repository to clone, no objects to decompress, no working tree to update. The entire “update” operation is downloading a single 3MB file. On any reasonable connection, that takes under two seconds. On a fast connection, it’s under one.
If brew update is the command you dread most, the solution isn’t a faster git or a cleverer workaround. It’s removing git from the equation entirely.
Need Rust performance engineering or AI agent expertise?
Neul Labs — the team behind stout — consults on Rust development, performance optimization, CLI tool design, and AI agent infrastructure. We build fast, reliable systems that ship.