Brew update: what it changes and what it leaves alone

brew update reads like a command that updates software. It does not. It updates the list of software that Homebrew knows about, and leaves every installed package exactly as it was. That single distinction accounts for most of the confusion around it, including the experience of running it, seeing a long list of formula names scroll past, and finding afterwards that nothing on the Mac changed. Everything else worth knowing sits around the edges: when it runs without being asked, what the next command drags along with it, and what gets deleted quietly afterwards.

The two commands do unrelated jobs

Homebrew's own manual page is precise about the first one:

Fetch the newest version of Homebrew and all formulae from GitHub using git(1) and perform any necessary migrations. Source: docs.brew.sh

Nothing in that sentence touches an installed package. What changes is Homebrew itself, the definitions it reads, and any internal migrations required by a new Homebrew version. The list of names that scrolls past is a report of which definitions changed upstream, not a report of work performed locally.

brew upgrade is the command that changes installed software. The manual describes it as upgrading outdated, unpinned packages using the same options they were originally installed with. Between the two sits brew outdated, which lists installed packages with a newer version available and does nothing else. Homebrew's FAQ names the sequence directly: brew update first, then brew outdated to see what is affected, then brew upgrade to act.

Command What it reads What it changes Safe to run at any time
brew update GitHub Homebrew and formula definitions Yes
brew outdated Local state and definitions Nothing Yes
brew upgrade Local state and definitions Installed packages No, it replaces software
brew cleanup Local state Deletes old versions and cached downloads No, it deletes

The phrase about migrations in the manual is worth a moment. A Homebrew release can change where things live, rename a tap, or move a formula from one tap to another, and those changes have to be applied to the local installation rather than merely described in a definition. That work happens during brew update, which is why an installation left alone for several months can take noticeably longer on its first update than on the second one the same day.

The reason brew update is worth running on its own is that brew outdated compares installed versions against the definitions currently on disk. Skipping the update means comparing against yesterday's catalogue, which produces an answer that looks authoritative and is simply stale.

Auto-update has probably already run it

A second reason the command feels inert is that it frequently is, because Homebrew ran it already. HOMEBREW_AUTO_UPDATE_SECS controls how often brew update runs on its own before commands such as brew install, brew upgrade or brew tap. The default is 86400 seconds, which is 24 hours. That default drops to 3600 seconds, one hour, once a developer command has been run, and to 300 seconds, five minutes, when HOMEBREW_NO_INSTALL_FROM_API is set.

There is a second and shorter clock. Since Homebrew began serving formula and cask metadata through an API rather than requiring full local checkouts, HOMEBREW_API_AUTO_UPDATE_SECS governs how often that data is refreshed, and its default is 450 seconds. So a Mac that has not seen an explicit brew update in a week may still have metadata that is minutes old.

The developer mode detail catches people who once ran brew edit to look at a formula. Homebrew's FAQ notes that running a developer command enables developer mode, which both shortens the auto-update interval to hourly and changes what updates track: the latest commit on main instead of the latest stable tag. brew developer off reverses it. Setting HOMEBREW_UPDATE_TO_TAG=1 keeps the hourly interval but returns to stable tags.

Three switches cover the rest. HOMEBREW_NO_AUTO_UPDATE disables the automatic run entirely, though Homebrew recommends raising HOMEBREW_AUTO_UPDATE_SECS instead, and warns that combining the variable with new taps can produce a broken configuration. HOMEBREW_AUTO_UPDATE_QUIET keeps the automatic run but suppresses its report of new, outdated and deleted packages. For scripts, brew update-if-needed exists specifically so that the no-op case is both possible and fast, which is a better fit than either disabling auto-update or forcing a full update on every run.

What brew upgrade drags along

The most common surprise is scope. Asking for one package and watching a dozen others upgrade is not a bug, and the FAQ explains the reasoning:

Homebrew doesn't support arbitrary mixing and matching of formula versions, so everything a formula depends on, and everything that depends on it in turn, needs to be upgraded to the latest version. As a consequence any given upgrade or install command can upgrade many other (seemingly unrelated) formulae, especially if something important like python or openssl also needed an upgrade. Source: docs.brew.sh

There is a second mechanism on top of that. Unless HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK is set, brew upgrade also runs for outdated dependents, and brew reinstall runs for dependents with broken linkage. Setting that variable reduces the collateral work, at the cost of more breakage later, because a package built against a library that has since moved will keep failing until it is rebuilt.

The answer is to look before acting, and brew upgrade -n does exactly that. The --dry-run flag shows what would be upgraded without upgrading anything, which converts an unpleasant surprise into a list that can be read. brew outdated --verbose gives the same list with version numbers from the other direction.

Two smaller flags are worth knowing. Ask mode is now the default, so brew upgrade prompts for confirmation before downloading, and -y or --no-ask skips the prompt for unattended use. --minimum-version upgrades a named package only if the installed version is below a given floor, which is the precise tool for the case where a specific bug fix is needed and nothing more.

Cleanup happens whether or not it is asked for

The third command in this family runs on its own, and it deletes. Unless HOMEBREW_NO_INSTALL_CLEANUP is set, brew cleanup runs after brew install, brew upgrade and brew reinstall for the packages just touched, and for all packages once HOMEBREW_CLEANUP_PERIODIC_FULL_DAYS has elapsed. That variable defaults to 30 days. The FAQ states the consequence plainly: old versions of each upgraded formula are uninstalled automatically, with additional cleanup every 30 days.

Run by hand, brew cleanup removes stale lock files, outdated downloads, and old versions of installed formulae. It removes downloads older than 120 days, adjustable with HOMEBREW_CLEANUP_MAX_AGE_DAYS. --prune takes a number of days, or all. -s scrubs the cache including downloads for current versions, though downloads for anything actually installed survive even then. As with upgrades, -n shows what would go without removing it, and that is the version to run first on a machine where disk space is the reason for running it at all.

The download cache lives where brew --cache reports, usually ~/Library/Caches/Homebrew. Inspecting it before scrubbing it is reasonable, and it is one of the places where reading a directory listing and running the matching command in the same window saves a trip, which is the argument for a file manager with a built-in terminal rather than two applications.

Disabling cleanup has a consequence that the FAQ flags as surprising. With automatic cleanup off, uninstalling a formula removes only the most recent installed version, leaving older ones behind, while brew upgrade continues to pursue the newest version it knows about. brew uninstall --force removes a formula entirely in that situation. HOMEBREW_NO_CLEANUP_FORMULAE takes a comma-separated list, which is the narrower tool when only one or two packages need their old versions kept.

Holding a version still

brew pin prevents a package from being upgraded by brew upgrade, and brew unpin releases it. Two limits are worth stating before relying on it. A pinned, outdated formula that another formula depends on still has to be upgraded when required, because Homebrew does not build against outdated versions. And a pinned cask is skipped by brew upgrade, but the application's own updater may still update it outside Homebrew entirely, which is a different mechanism that a pin cannot reach.

Pinning also does not stop Homebrew learning that a newer version exists. Only HOMEBREW_NO_AUTO_UPDATE does that, and the FAQ pairs the two when answering how to stop something from being updated. For a version that genuinely must not move, Homebrew's own documentation on locking installed formulae at specific versions describes the trade-offs, and the honest summary is that a package manager built around one tested combination of versions is not the ideal place to freeze one component of it.

Casks follow different rules

A cask that ships as version :latest, or that declares auto_updates true, is skipped by brew upgrade by default, because in both cases Homebrew cannot tell whether an upgrade is needed or the application handles it internally. --greedy includes both. --greedy-latest and --greedy-auto-updates take one of those cases each. The same flags apply to brew outdated, so brew outdated --cask --greedy is the honest inventory of what is behind on the applications side.

One more asymmetry is worth noting. A formula upgrade replaces files under the Homebrew prefix, where nothing else is competing for the same paths. A cask upgrade replaces an application bundle in the Applications folder, which the system, Launch Services and any running copy of the application all have opinions about. That is the reason cask upgrades fail in ways formula upgrades do not, and why checking what is actually running before a large cask upgrade is time well spent.

By default, running cask applications are quit during an upgrade. --no-quit prevents that, and HOMEBREW_NO_UPGRADE_QUIT_CASKS makes it the default. On a Mac with long-running work in progress, setting it is the difference between an upgrade that is a background task and one that closes something mid-session.

When brew update itself is what is broken

Occasionally the update is the failure rather than the fix, usually because a tap repository has local changes or a failed merge. brew update -v prints the directories checked and the git operations performed, which is normally enough to identify which repository is stuck. brew update --force performs the full, slower check even when Homebrew believes it is unnecessary.

brew update-reset is the last resort. It fetches and resets Homebrew and all tap repositories to their latest origin/HEAD, and the manual carries an explicit warning that this destroys all uncommitted or committed changes. On a machine where nobody has edited a formula that is exactly what is wanted. On a machine with a local tap holding work that exists nowhere else, it is not, and the tap is worth pushing somewhere first.

What to change first

Run brew update, then brew outdated, then brew upgrade -n and read the list before running the upgrade for real. If a large upgrade at an inconvenient moment is the recurring complaint, raise HOMEBREW_AUTO_UPDATE_SECS rather than disabling auto-update, and set HOMEBREW_NO_UPGRADE_QUIT_CASKS so an upgrade never closes an application mid-task. Keeping that output and the directory it affects in one window is what Atriens is built around.

Frequently asked questions

What is the difference between brew update and brew upgrade?

brew update fetches the newest version of Homebrew and all formula definitions from GitHub and changes no installed software. brew upgrade replaces installed packages with newer versions. Running the first alone will never change what is on the machine, which is why nothing appears to happen.

Does brew update need to be run before brew upgrade?

In practice it is already running. Homebrew auto-updates before commands such as brew install and brew upgrade once HOMEBREW_AUTO_UPDATE_SECS has elapsed, which defaults to 24 hours, and refreshes API metadata every 450 seconds by default. Running it explicitly guarantees brew outdated is comparing against current definitions rather than yesterday's.

Why does upgrading one package upgrade ten others?

Homebrew does not support arbitrary mixing of formula versions, so dependencies and dependents are pulled to the latest tested combination. Something widely depended on, such as python or openssl, will therefore cascade. brew upgrade -n shows the full list before anything is downloaded.

How can one package be held at its current version?

brew pin stops brew upgrade touching it. Two limits apply: a pinned formula that another formula depends on still has to be upgraded when required, and a pinned cask can still be updated by the application's own updater outside Homebrew.

Is it necessary to run brew cleanup manually?

Usually not. Unless HOMEBREW_NO_INSTALL_CLEANUP is set, cleanup runs after installs and upgrades for the affected packages, and for everything once 30 days have passed. Running brew cleanup -n by hand is still useful to see what would be removed, since cached downloads older than 120 days are included.

Why does Homebrew update every hour instead of every day?

Running a developer command such as brew edit or brew create enables developer mode, which shortens the interval to one hour and switches updates to tracking the latest commit on main rather than the latest stable tag. brew developer off restores the default, and HOMEBREW_UPDATE_TO_TAG=1 returns to stable tags on their own.

Back to all posts