Three commands, three different approaches to stopping processes. They look similar but behave differently in ways that matter.
kill: signal by PID
kill sends a signal to a specific process by PID. That’s it. Despite the name, it doesn’t always kill — it’s really a signal delivery mechanism.
kill 12345 # send SIGTERM (default)
kill -9 12345 # send SIGKILL
kill -HUP 12345 # send SIGHUP (reload config)
kill -STOP 12345 # freeze the process
kill -CONT 12345 # resume it
The common signals:
| signal | number | catchable? | use case |
|---|---|---|---|
| SIGTERM | 15 | yes | ask process to exit cleanly |
| SIGKILL | 9 | no | force kill, no cleanup |
| SIGINT | 2 | yes | same as Ctrl+C |
| SIGHUP | 1 | yes | reload config / terminal hangup |
| SIGSTOP | 19 | no | freeze process |
| SIGCONT | 18 | no | resume frozen process |
kill is the safest option because you’re explicit about which PID gets the signal. No pattern matching, no ambiguity. The downside: you need the PID first.
# typical workflow
ps aux | grep node
# read the PID from the output
kill 12345
You can signal multiple PIDs:
kill 12345 12346 12347
kill -9 $(pgrep -f "webpack-dev-server")
pkill: signal by pattern
pkill matches process names (or command lines) against a regex pattern and signals every match.
pkill node # SIGTERM to all processes named "node"
pkill -9 webpack # SIGKILL to all processes matching "webpack"
pkill -f "npm start" # match against full command line
Useful flags:
| flag | description |
|---|---|
-f |
match against full command line, not just process name |
-x |
exact match only (no partial matching) |
-u user |
only processes owned by this user |
-t tty |
only processes on this terminal |
-P pid |
only children of this parent PID |
-n |
only the newest matching process |
-o |
only the oldest matching process |
the regex gotcha
pkill uses regex matching by default, and it matches substrings:
pkill node
This kills processes named node, but also nodemon, nodecellar, and anything else containing “node.” If you want an exact match:
pkill -x node
The -f flag is powerful but dangerous. pkill -f "server" matches any process whose command line contains “server” — which could be a lot of things.
pgrep: the read-only sibling
pgrep uses the same matching logic as pkill but only prints PIDs. Use it to preview what pkill would hit:
pgrep -la node # list PIDs and names
pgrep -fa "npm start" # list PIDs and full commands
Always run pgrep before pkill if you’re unsure what will match.
killall: signal by exact name
killall matches by process name — but unlike pkill, it matches the exact process name (on Linux), not a regex substring.
killall node # all processes exactly named "node"
killall -9 firefox # force kill firefox
Useful flags:
| flag | description |
|---|---|
-w |
wait for processes to die before returning |
-I |
case-insensitive matching (Linux) |
-v |
report whether the signal was sent successfully |
-r |
use regex matching (Linux, makes it behave more like pkill) |
the macOS warning
On older macOS systems (and other BSDs), killall without arguments kills all processes you own. This is not a theoretical risk — it will log you out or worse. Modern macOS requires a process name argument, but the reputation sticks.
More importantly, killall on macOS matches differently than on Linux. On macOS, it does substring matching by default. On Linux, it’s exact. This means:
killall node # Linux: only "node", not "nodemon"
killall node # macOS: could match "nodemon" too
If you write scripts that run on both platforms, pkill -x is more predictable.
the -w flag
One genuinely useful killall feature: the -w (wait) flag blocks until all matched processes have exited. Useful in scripts where you need to know the process is truly gone before continuing:
killall -w node && echo "all node processes stopped"
Neither kill nor pkill has this built in.
when to use each
Use kill when you know the exact PID and want maximum control. Best for interactive use where you’ve already identified the process.
Use pkill when you want to match by name, command line, user, or parent. Best for scripts and automation. Always preview with pgrep first.
Use killall when you want exact name matching and the -w (wait) flag. Be aware of macOS/Linux differences. Avoid in cross-platform scripts.
with proc
proc handles name matching, port targeting, and multi-target resolution with deduplication and confirmation prompts:
proc kill node # kill all node processes (with confirmation)
proc stop :3000 # graceful stop by port
proc kill :3000,webpack # multiple targets, deduplicated
proc kill node --dry-run # preview what would be killed
Explicit intent, no regex surprises, confirmation before anything dies.
Install
brew install yazeed/proc/proc # macOS
cargo install proc-cli # Rust
npm install -g proc-cli # npm/bun
See the GitHub repo for all installation options.