In the macOS operating system, background automation, scheduled cron replacements, and persistent system service execution are managed entirely by Apple’s unified service framework: launchd. Whether an application installs a helper tool to perform automated software updates, a network utility synchronises corporate credentials, or a developer scripts a nightly backup pipeline, background services are defined using Property List (.plist) configuration files. Interfacing with launchd requires using the native launchctl command-line utility. However, many guides on the internet continue to teach deprecated commands (such as launchctl load and unload) that produce erratic results on modern macOS versions. Mastering the modern subcommands—including bootstrap, bootout, and print—provides complete administrative control over background processes in macOS.
Understanding LaunchDaemons vs LaunchAgents and Domain Targets
Apple’s launchd architecture categorises background jobs into two functional classes based on their execution context and user session boundaries:
- LaunchDaemons: System-wide background services that execute with root privileges in the system context. They initialize during the boot sequence prior to user login and run continuously in the background without user interface interaction. Stored primarily in
/Library/LaunchDaemons/(for third-party daemons) and/System/Library/LaunchDaemons/(for protected Apple system daemons). - LaunchAgents: User-scoped services that execute within the context of a logged-in user session with standard user privileges. Stored in
/Library/LaunchAgents/(global agents executing for all users) and~/Library/LaunchAgents/(executing strictly for the individual user account).
Modern launchctl commands require specifying a target domain, which tells launchd exactly where to host the job:
system/:The root system execution domain for LaunchDaemons.gui/<UID>/:The graphical user session domain (e.g.gui/501) for interactive LaunchAgents.user/<UID>/:The non-graphical background user domain for headless background tasks.
Listing Active Services and Querying Status via launchctl list
To inspect currently registered background services within your active terminal context, invoke the list verb:
launchctl list
The resulting table contains three columns:
- PID: The operating system process ID if the job is actively executing. If the service is idle or configured to trigger only on scheduled intervals, this column displays a hyphen (
-). - Status: The exit code of the last process termination. A status of
0indicates successful execution. A non-zero positive integer indicates an application exit code (e.g.1for generic failure), while negative numbers indicate fatal termination by Unix signals (e.g.-9for SIGKILL or-15for SIGTERM). - Label: The unique reverse-DNS identifier of the job (e.g.
com.docker.helperorcom.apple.metadata.mds).
Inspecting In-Depth Service Telemetry Using launchctl print
The modern print subcommand outputs an exhaustive diagnostic report detailing a service’s runtime environment, path configurations, environment variables, resource limits, and historical crash counts.
Inspecting a System-Level LaunchDaemon
sudo launchctl print system/com.example.systemdaemon
Inspecting a User-Level LaunchAgent
launchctl print gui/$(id -u)/com.example.useragent
Key diagnostic telemetry fields within the print output include:
- state: Indicates whether the daemon is
running,waiting, ordisabled. - runs: Total count of times the service has executed since system boot.
- last exit code: Precise error status returned during the most recent termination.
- endpoints: Registered Mach ports and XPC service listener handles.
Loading and Starting Services Using the Modern bootstrap Verb
The legacy launchctl load command is officially deprecated. Modern macOS workflows utilize the bootstrap subcommand to register new configuration plists into their designated domain.
Bootstrapping a System-Wide LaunchDaemon
- Ensure the target plist file resides in
/Library/LaunchDaemons/and possesses strict root ownership:sudo chown root:wheel /Library/LaunchDaemons/com.custom.backup.plist sudo chmod 644 /Library/LaunchDaemons/com.custom.backup.plist - Register the daemon into the
systemdomain:sudo launchctl bootstrap system /Library/LaunchDaemons/com.custom.backup.plist
Bootstrapping a User Session LaunchAgent
- Place the plist in your user directory:
~/Library/LaunchAgents/com.custom.sync.plist. - Register the agent into your graphical user domain without using sudo:
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.custom.sync.plist
Stopping and Unloading Services Using the bootout Verb
To safely terminate a running daemon and remove its registration from launchd, employ the bootout subcommand.
Unloading by Service Label
sudo launchctl bootout system/com.custom.backup
Unloading by Configuration File Path
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.custom.sync.plist
The bootout command immediately dispatches a termination signal to the active process, deregisters its Mach communication ports, and purges the job from active memory.
Troubleshooting Common Service Failures and Crash Loops
When a background job repeatedly terminates with non-zero exit codes, investigate the following failure modes:
- File Ownership Mismatch: System LaunchDaemons in
/Library/LaunchDaemons/MUST be owned byroot:wheelwith permissions no more permissive than644. If permissions are set to world-writable (e.g.777),launchdrefuses to bootstrap the file for security reasons. - Missing WorkingDirectory: If a script relies on relative file paths, ensure the plist explicitly defines the
<key>WorkingDirectory</key>tag. - Standard Error Logging: Always configure explicit logging paths within your plist to capture crash telemetry:
<key>StandardOutPath</key> <string>/tmp/myjob.stdout.log</string> <key>StandardErrorPath</key> <string>/tmp/myjob.stderr.log</string>
Utilising modern launchctl commands ensures robust service orchestration, reliable background automation, and clean diagnostic visibility across macOS.