How Homebrew Bottles Cut Install Times from Minutes to Seconds
Homebrew bottles replace source compilation with pre-built binaries, delivering 10–50x faster installs. This post explains how bottles are built, selected, and relocated—and when you still need --build-from-source.
28 Oct 2025, 20:30 UTC

The Problem: Waiting for Source Builds
Every Homebrew user knows the frustration: you run brew install wget and watch the terminal scroll through configure scripts, compiler invocations, and linker errors for 45 seconds—or minutes for larger packages. On Apple Silicon, some formulae still lack optimized builds, forcing Rosetta translation or full source compilation. This isn't just an annoyance; it slows CI pipelines, onboarding, and daily workflow.
The solution Homebrew adopted is bottles: pre-compiled binary archives tied to a specific formula version, macOS/Linux release, and CPU architecture. When a matching bottle exists, Homebrew downloads and extracts it into the Cellar, skipping the entire build step. The result is a 10–50x speedup for most packages.
How Bottles Work Under the Hood
Each formula in homebrew/core declares a bottle do ... end block listing a root_url (now ghcr.io/v2/homebrew/core/<formula>), a SHA-256 per platform, and a rebuild number. When you run brew install foo, the client fetches a bottle JSON index, matches your OS version and architecture (e.g., arm64_sonoma), and downloads the corresponding .tar.gz if --build-from-source isn't forced.
Bottles are built automatically via GitHub Actions. The homebrew/core repository runs a matrix job on macos-13 (Intel), macos-14 (ARM), and Ubuntu runners. Successful builds publish artifacts as GitHub Packages attached to the PR or release. Maintainers then run brew bottle --merge to inject the new SHA-256 hashes into the formula.
Relocatability: Why the Same Archive Works Anywhere
A bottle built on a runner with HOMEBREW_PREFIX=/opt/homebrew must also work if you installed Homebrew to /usr/local or a custom path. Homebrew achieves this by compiling with -Wl,-rpath,@rpath on macOS and using install_name_tool (or patchelf on Linux) to rewrite dynamic library references to @rpath/../lib/libfoo.dylib. At pour time, the bottler sets the rpath to the actual Cellar location. Run otool -L $(which wget) and you'll see @rpath entries instead of hardcoded paths.
Worked Example: wget on an M2 Mac
On a macOS 14 (Sonoma) Apple Silicon machine:
$ brew install --verbose wget
==> Downloading https://ghcr.io/v2/homebrew/core/wget/manifests/1.24.5
==> Downloading https://ghcr.io/v2/homebrew/core/wget/blobs/sha256:...
==> Pouring wget-1.24.5.arm64_sonoma.bottle.tar.gz
🍺 /opt/homebrew/Cellar/wget/1.24.5: 50 files, 4.2MB, built in 2 seconds
The bottle is ~1.2 MB and pours in ~2 seconds. Compiling from source with brew install --build-from-source wget takes ~45 seconds and pulls dependencies like openssl@3 and libidn2. You can verify the bottle metadata with brew info --json=v2 wget and inspect the bottle.stable object for supported platforms and SHA-256 values.
Trade-offs and Limitations
- Storage and rebuild burden: Core bottles consume ~500 GB on GitHub Packages. When a shared dependency like OpenSSL gets a security update, maintainers must bump the
rebuildnumber on dozens of formulae and re-run the bottle matrix. - SDK compatibility: A bottle built on the macOS 14 SDK may not run on macOS 11 if it references newer symbols. Homebrew sets minimum deployment targets (
-mmacosx-version-min) to mitigate this, but edge cases exist. - Linux maturity: Fewer formulae have Linux bottles, and glibc version mismatches can cause runtime failures on older distributions.
- Privacy: Bottle pour events are reported to Homebrew's analytics (opt-out via
HOMEBREW_NO_ANALYTICS=1). Air-gapped or compliance-sensitive environments should disable analytics and considerHOMEBREW_NO_INSTALL_FROM_API=1to avoid the JSON API entirely. - Hardcoded paths in third-party taps: Some older or non-core formulae still embed absolute prefixes, breaking relocatability.
When to Build from Source
Use brew install --build-from-source <formula> when you need custom compiler flags (-march=native, sanitizers), target an architecture without bottles (e.g., Linux ARMv7), or suspect a bottle mismatch. For reproducible builds in CI, pin the bottle SHA-256 in your Brewfile with brew 'wget', bottle: { sha256: '...' }.
To verify your setup: run brew config to confirm HOMEBREW_PREFIX, then otool -L $(which wget) to see @rpath entries. If you're on Linux, readelf -d $(which wget) | grep RUNPATH should show $ORIGIN/../lib.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.