Ending a process on macOS is two steps: find it, then send it a signal. ps, pgrep, and lsof find it; kill, pkill, and killall end it. The three “kill” commands differ only in how you name the target — a PID, a pattern, or an exact name — and picking the right one is most of the job. Everything here was run on macOS 26.5.2.
Find the process
By name, pgrep returns the PID; add -l to see the command and -f to match against the whole command line, not just the process name:
$ pgrep -fl Safari
594 /Applications/Safari.app/Contents/MacOS/Safari
By what’s holding a port — the usual reason a dev server won’t start — lsof with -t prints just the PID:
$ lsof -ti :3000
85714
kill, pkill, killall: pick your target
All three end a process. They differ in how you point at it:
kill 594 # a single PID
pkill -f "node server.js" # anything matching a command-line pattern
killall Safari # every process with that exact name
kill takes the PID you already found. pkill matches by process name, or with -f the full command line, which is what you want when several processes share one launcher. killall takes an exact name and ends every instance of it — exact matters: killall "Google Chrome" works, killall chrome matches nothing.
All three send SIGTERM (signal 15) by default, which asks a process to shut down and lets it save first. When something is genuinely stuck and ignores that, -9 sends SIGKILL, which can’t be caught — it stops the process at once with no chance to clean up, so it’s the last resort, not the default:
kill -9 594
killall -9 "App Name"
The two cases this comes up most
A port is “already in use.” Find what’s on it and end that one process in a single line:
lsof -ti :3000 | xargs kill
lsof -ti prints only the PID(s) bound to port 3000, and xargs passes them to kill. If a plain kill doesn’t free the port, repeat it as lsof -ti :3000 | xargs kill -9.
An app is frozen and won’t quit. End every process running under its name at once:
killall -9 "App Name"
Get the exact name from Activity Monitor, or from pgrep -fl before you kill, so killall has something to match.
launchctl for background services
Ordinary kill handles running programs. Background services managed by launchd — the ones defined by .plist files in LaunchAgents and LaunchDaemons — are handled with launchctl. List what’s loaded, filtered to what you care about:
launchctl list # PID Status Label, one row per service
launchctl list | grep myservice
Load or unload a service from its plist, and start or stop one that’s already loaded:
launchctl load ~/Library/LaunchAgents/com.example.agent.plist
launchctl unload ~/Library/LaunchAgents/com.example.agent.plist
launchctl start com.example.agent
launchctl stop com.example.agent
These load, unload, start, and stop forms are what most setup instructions use, and they work. macOS also has newer domain-based commands — launchctl bootstrap, bootout, and kickstart -k to restart a service in place — which mainly matter when a service ignores the older forms or has to be targeted in a specific login or GUI session.
Day to day that’s the whole toolkit: pgrep and lsof to find a process, kill / pkill / killall to end it, -9 when it won’t listen, and launchctl for the background services kill shouldn’t touch.