Why Is Homebrew So Slow? A Technical Deep Dive
Homebrew's slowness comes from Ruby startup overhead, a 700MB git-based metadata repo, and sequential downloads. Here's exactly why — and what stout does differently.
If you’ve used Homebrew for more than a week, you’ve felt it: the multi-second pause before anything happens, the glacial brew update, the one-at-a-time package downloads. Homebrew is the most popular package manager on macOS, but it is also one of the slowest. This isn’t a matter of perception — there are concrete architectural reasons why every brew command carries measurable overhead.
Let’s walk through the five biggest bottlenecks, with real numbers, and then look at what a ground-up redesign can do about them.
1. Ruby interpreter startup (~500ms per invocation)
Every time you run any brew command — even brew --version — the system launches a Ruby interpreter. On a modern M2 MacBook Pro, this takes roughly 450-550ms before a single line of Homebrew’s own code executes.
You can measure this yourself:
time brew --version
# Homebrew 4.x.x
# real 0m0.52s
Compare that to a native compiled binary:
time stout --version
# stout 0.x.x
# real 0m0.005s
The difference is 100x — and it compounds. A script that calls brew info in a loop to check ten packages spends five seconds just booting Ruby.
Ruby is an interpreted language with a relatively heavy startup path. It loads the interpreter, parses the Homebrew library files, initializes global state, and configures the environment before any actual work begins. This was a reasonable tradeoff in 2009 when Homebrew was a clever hack for installing Unix tools on macOS. At scale, with developers running dozens of brew commands per day, it’s a tax on every interaction.
2. The 700MB+ homebrew-core git repository
Homebrew stores all of its package metadata — every formula for every version — in a git repository called homebrew-core. As of early 2026, this repository contains over 170,000 formulae and weighs in at roughly 700MB on disk after a fresh clone.
When you run brew update, Homebrew performs a git fetch and git merge on this repository. If you haven’t updated in a few days, this can involve downloading tens of megabytes of git objects and rebasing local changes. The operation routinely takes 10-60 seconds depending on how stale your local clone is:
time brew update
# Already up-to-date.
# real 0m12.4s # Even when there's nothing new
The git approach has an additional cost: disk I/O. Git needs to traverse the object graph, decompress objects, and update the working tree. On spinning disks or resource-constrained CI runners, this is even more painful.
3. Sequential bottle downloads
When you install a package with dependencies — say, brew install ffmpeg — Homebrew downloads each pre-built bottle one at a time. ffmpeg pulls in dozens of dependencies (libx264, libx265, opus, lame, and more). Each download waits for the previous one to finish before starting.
On a fast connection, individual bottle downloads might take 1-3 seconds each. But with 20+ dependencies, the sequential chain adds up to 30-60 seconds of wall-clock time — even though your network could easily handle all those downloads in parallel.
This is a fundamental architectural limitation. Homebrew’s Ruby codebase uses synchronous I/O for downloads. Each bottle is fetched, verified, and extracted before the next one begins.
4. JSON API parsing overhead
Homebrew migrated from reading formulae files directly to using a JSON API (formulae.brew.sh) for package metadata. While this was an improvement over evaluating thousands of Ruby files, parsing large JSON payloads in Ruby is not fast. A full search operation requires loading and parsing a multi-megabyte JSON document:
time brew search json
# real 0m2.8s
The JSON API response for all formulae is several megabytes uncompressed. Ruby’s JSON parser, even with native extensions, adds hundreds of milliseconds to deserialize this data into Ruby objects, iterate over them, and perform string matching.
5. Auto-update overhead
By default, Homebrew runs brew update automatically if it hasn’t been run in the last 24 hours. This means the first brew install or brew search of the day silently triggers the full git pull described above, adding 10-60 seconds before your actual command even starts.
You can disable this with HOMEBREW_NO_AUTO_UPDATE=1, but then you risk installing outdated packages or missing security updates. It’s a lose-lose: either you pay the time tax or you fly blind.
The compounding effect
These aren’t isolated issues — they stack. A simple brew install ripgrep on a fresh day involves:
- Ruby startup: ~500ms
- Auto-update git pull: 10-30s
- JSON API fetch and parse: 1-2s
- Dependency resolution: 0.5-1s
- Sequential bottle download: 3-5s
- Extraction and linking: 1-2s
Total: 15-40 seconds to install a 3MB binary.
How stout solves each bottleneck
stout was designed from the ground up to eliminate every one of these bottlenecks:
Native Rust binary instead of Ruby. stout compiles to a single statically-linked executable. Startup time is ~5ms — there is no interpreter, no library loading phase, no global state initialization. This alone is a 100x improvement on every single command.
3MB SQLite index instead of 700MB git repo. stout replaces the homebrew-core git repository with a pre-computed SQLite database, compressed with zstd to roughly 3MB. This database includes FTS5 full-text search indices, pre-resolved dependency graphs, and bottle URLs. Updating the index means downloading a single small file — typically 1-3 seconds instead of 10-60.
time stout update
# Index updated (3.1 MB, 172,431 formulae)
# real 0m1.2s
Parallel downloads with Tokio. stout uses Rust’s Tokio async runtime to download multiple bottles simultaneously. When installing ffmpeg with 25 dependencies, stout fetches all bottles concurrently, saturating your network connection instead of serializing requests.
Pre-indexed search. Because the SQLite database includes FTS5 search tables, queries execute in milliseconds without parsing any JSON:
time stout search json
# real 0m0.04s
No auto-update penalty. stout checks for index staleness using a lightweight HTTP HEAD request (a few milliseconds). If the index is current, there is no update at all. If it’s stale, the 3MB download happens in the background while your command proceeds.
The bottom line
Homebrew’s slowness is not a bug — it’s a consequence of architectural decisions made over a decade ago. Ruby startup, git-based metadata, and sequential I/O were acceptable tradeoffs when the formula count was small and developer patience was high. They don’t scale to 170,000+ packages and the expectation of instant tooling.
stout keeps full compatibility with Homebrew’s package ecosystem — same bottles, same Cellar, same taps — while replacing the slow parts with a purpose-built architecture. The result is 10-100x faster for the operations developers run most often.
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.