Managing Background Daemons on macOS with brew services and launchd
Learn how brew services wraps launchd to run PostgreSQL and other daemons on macOS, including plist inspection, verification commands, and common pitfalls.
10 Nov 2025, 02:34 UTC

The problem: daemons that die with your terminal
If you install PostgreSQL, Redis, or another long-running server with Homebrew and then close your terminal, the process usually stops. You can restart it by hand, but that is fragile: you must remember the exact binary path, the working directory, and any environment variables. The useful decision is to let brew services manage the daemon through macOS's native service manager, launchd. This gives you short start/stop commands and automatic property list (plist) handling, so the service survives terminal restarts and reboots.
What brew services actually does
brew services is not a replacement for launchd. It is a wrapper that reads a service block from a Homebrew formula, writes a launchd property list (plist) file to ~/Library/LaunchAgents/, and then calls launchctl—the command-line interface to launchd—to load or unload that file.
A plist is a structured file, usually XML, that tells launchd which executable to run, what arguments to pass, whether to restart the process if it crashes, and where to send standard output and error streams. Once loaded, launchd owns the process lifecycle. You do not need to keep a shell session open.
Worked example: running PostgreSQL as a service
This example assumes you have already installed the formula. If not, install it first:
brew install postgresql
Run this in your normal user shell. No sudo is required, because brew services creates a launch agent that runs in your user session rather than a system-wide daemon.
Start the service
brew services start postgresql
This command performs two actions. First, it generates ~/Library/LaunchAgents/org.homebrew.postgresql.plist. Second, it submits that file to launchd via launchctl. If the formula does not contain a service definition, brew services reports an error instead of creating a plist.
Inspect the generated plist
cat ~/Library/LaunchAgents/org.homebrew.postgresql.plist
The file contains keys that control launchd's behavior. Common ones include:
| Key | Purpose |
|---|---|
ProgramArguments | The executable path and arguments launchd runs |
KeepAlive | Whether launchd restarts the process after it exits |
StandardOutPath | File where standard output is written |
StandardErrorPath | File where standard error is written |
WorkingDirectory | Directory from which the process starts |
The exact values depend on the formula. You should not edit this file by hand for routine changes; instead, stop the service, adjust the formula or environment, and start again.
Verify it is loaded
Check that launchd recognizes the label:
launchctl list | grep postgresql
Expected check: a line containing org.homebrew.postgresql. The second column shows the last exit status, and the third shows the process ID (PID) if the service is currently running.
Check Homebrew's own view:
brew services list
Expected check: a row for postgresql with status started.
If PostgreSQL is running, its default port should be listening. You can confirm with:
lsof -i :5432
Expected check: at least one process bound to the port, typically owned by your user.
Stop the service
brew services stop postgresql
This unloads the plist from launchd. The plist file remains on disk so that a later brew services start can reuse it. If you want to remove the service definition entirely, delete the plist file manually after stopping it.
Limits and common mistakes
macOS only
brew services depends on launchd and launchctl, which exist only on macOS. On Linux, Homebrew does not provide this subcommand for service management. You must use systemd, OpenRC, or another init system instead.
The formula must declare a service block
Not every formula includes a service definition. If you run brew services start <formula> for a formula that lacks one, the command returns an error indicating no service definition was found. Before relying on brew services, check whether the formula provides one.
User agents versus system daemons
By default, brew services creates a launch agent in your user context. That means the service starts when you log in and runs with your permissions. If you need a process to start before any user logs in—for example, a system-wide database—you would need a launch daemon placed in /Library/LaunchDaemons. brew services does not manage those by default, and using sudo brew services can create permission mismatches with Homebrew's own directory structure.
Environment inheritance
A launchd service does not read your shell's startup files. Variables you export in .zshrc or .bash_profile are not present when launchd starts the process. If a formula depends on a custom PATH or other environment variable, set it inside the formula or use launchctl setenv before starting the service.
Duplicate processes
If you start a server manually and then start it again with brew services, you may end up with two processes competing for the same port. Always stop manual instances before switching to service management.
Stale plist files
Uninstalling a formula does not always delete its plist from ~/Library/LaunchAgents/. If you reinstall later, a stale file can cause confusion. Remove the plist manually if you want a clean slate.
A practical verification checklist
After starting a service, confirm three things:
launchctl list | grep <formula>shows the expected label.brew services listreports the status asstarted.- The application is reachable on its expected port or socket.
If the service fails to start, read the paths listed under StandardOutPath and StandardErrorPath in the generated plist. Those files usually contain the actual error, such as a missing data directory or a port already in use.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.