command not found: timeout
Every Linux answer wraps long-running commands in timeout. macOS has never shipped it. Four replacements, and the one that behaves correctly when the command exits on its own.
timeout 60 ./slow-thing
zsh: command not found: timeout
macOS does not have it, and never has. gtimeout is not there either unless you installed Homebrew’s coreutils. The command is part of GNU coreutils and the macOS userland comes from BSD, so this is a lineage difference rather than something broken on your machine.
Four replacements follow, in the order I would actually reach for them.
1. Homebrew coreutils
brew install coreutils
gtimeout 60 ./slow-thing
This is the real thing, prefixed with g so it does not shadow any system tool. It supports everything the GNU version does, including --kill-after for processes that ignore the first signal and --preserve-status for passing through the command’s own exit code.
It is the right answer on your own machine and the wrong answer in a script anyone else will run, because it fails on any Mac without coreutils installed. Which is most Macs.
2. perl, which macOS already ships
perl -e 'alarm shift; exec @ARGV' 60 ./slow-thing
alarm schedules a SIGALRM after the given number of seconds, then exec replaces the perl process with your command. If the command finishes first, nothing happens and you get its real exit code. If the alarm fires, the process dies with signal 14 and the shell reports exit status 142.
To match GNU’s convention of 124 on timeout:
timeout_run() {
local secs=$1; shift
perl -e 'alarm shift; exec @ARGV' "$secs" "$@"
local rc=$?
[ $rc -eq 142 ] && return 124
return $rc
}
timeout_run 60 ./slow-thing
No dependencies, correct exit codes, and perl has shipped with macOS for as long as anyone has been writing shell scripts on it. This is what I use when a script has to run on a machine I do not control.
The limitation is process trees. exec means there is one process to signal, so if your command spawns children they are not cleaned up when the alarm fires.
3. The background-and-kill one-liner, and why it is wrong
This is the version that comes up first in most search results:
./slow-thing & PID=$!
( sleep 60; kill $PID 2>/dev/null ) &
wait $PID
It does terminate the command after 60 seconds. It also reports the wrong outcome in the ordinary case.
When ./slow-thing finishes normally in 5 seconds, the sleep 60 subshell is still running. It wakes 55 seconds later and sends a signal to a PID that no longer belongs to your command. On a busy machine that PID may have been recycled, so the kill lands on something unrelated. Meanwhile wait has already returned, so the script continues while a stray timer is still armed.
The exit code is the more common problem. Depending on ordering, wait may report the status of the killed job rather than the completed one, so a command that succeeded is reported as terminated.
A corrected form cancels the timer:
./slow-thing & PID=$!
( sleep 60; kill -TERM $PID 2>/dev/null ) & WATCHER=$!
wait $PID; RC=$?
kill $WATCHER 2>/dev/null
wait $WATCHER 2>/dev/null
exit $RC
That works, and it is six lines to replace one word. The perl version is shorter and has fewer ways to go wrong.
4. Rewriting the problem away
Often the reason a command needs a timeout is that it is waiting on a network operation, and the tool already has a flag for that. Reaching for those first removes the need for any wrapper:
curl --max-time 60 https://example.org
ssh -o ConnectTimeout=10 host
ping -t 5 host
nc -w 5 host 443
Note that ping -t means something entirely different on Linux, where it sets the TTL rather than a deadline. This is the same BSD-versus-GNU divergence that removed timeout in the first place, showing up in the tool you reached for as a substitute.
Which to use
| Situation | Use |
|---|---|
| Your own machine, interactive | gtimeout after brew install coreutils |
| Script that must run on any Mac | the perl alarm function |
| Command spawns a process tree | gtimeout, and accept the dependency |
| The command is a network call | the tool’s own timeout flag |
The pattern worth avoiding is the background-and-kill snippet in its uncorrected form. It appears to work during testing, because in testing the command usually times out, which is the path that behaves correctly. The path that misbehaves is the one where everything went fine.
Checking what you have
command -v timeout gtimeout || echo "neither is installed"
On a stock macOS install that prints the fallback. If gtimeout appears, coreutils is present and you can use it, though your script still cannot assume anyone else can.
Testing your replacement before you rely on it
Whichever approach you choose, it is worth verifying both paths rather than only the one that prompted you to look. The failure mode described above survives testing precisely because people test the timeout path and not the completion path.
# Should exit 124 (or your chosen timeout code) after ~2 seconds
timeout_run 2 sleep 10; echo "timed out -> $?"
# Should exit 0 after ~1 second, with no stray timer left behind
timeout_run 5 sleep 1; echo "completed -> $?"
# Should preserve a non-zero exit code from the command itself
timeout_run 5 false; echo "failed cmd -> $?"
Three cases, three distinct expected results. A replacement that returns 0 for all three, or 124 for the second, is broken in a way that will eventually cost you a debugging session on a completely unrelated problem.
Timeouts inside a pipeline
A subtlety worth knowing: wrapping one stage of a pipeline does not bound the pipeline.
gtimeout 10 curl -s https://example.org | jq .
If curl is killed at ten seconds, jq receives whatever arrived and exits normally, so the
pipeline’s exit status is jq’s and the timeout is invisible. Under set -o pipefail the
status propagates, which is one of several reasons that option belongs near the top of any
script doing real work:
set -euo pipefail
Waiting on something other than a process
Frequently the actual need is not to bound a command but to wait for a condition with a deadline, and shell loops written for that tend to spin forever when the condition never arrives.
wait_for() {
local deadline=$(( $(date +%s) + $2 ))
until eval "$1"; do
[ "$(date +%s)" -ge "$deadline" ] && return 124
sleep 1
done
}
wait_for 'curl -sf http://localhost:4399 >/dev/null' 30 || echo "server never came up"
That polls a condition with a hard deadline and returns the same 124 convention. It needs no
external tool, works identically on Linux, and covers the case people usually reach for
timeout to solve.
Why macOS does not have it
The short answer is lineage. The macOS userland derives from BSD, and timeout is part of GNU
coreutils. It was never omitted so much as never inherited.
FreeBSD added its own timeout implementation in 2014, so the BSD family does have one now.
macOS did not pick it up, and the userland tools Apple ships have diverged from upstream FreeBSD
for long enough that this is unsurprising rather than an oversight.
The practical consequence is that this is not a gap that will close. Scripts written for macOS need one of the approaches above, and writing them portably in the first place costs less than discovering the difference on someone else’s machine.
Checking what is installed
command -v timeout gtimeout || echo "neither present"
On a stock macOS install that prints the fallback message. If gtimeout appears, Homebrew’s
coreutils is installed and you can use it interactively, though a script you intend to share
still cannot assume anyone else has it. The portable perl function above remains the safer choice
for anything that leaves your machine.
Takeaways
- macOS has never shipped GNU timeout, and gtimeout does not exist unless you installed coreutils.
- The widely copied background-and-kill one-liner reports success or failure incorrectly when the command exits before the timer.
- Homebrew coreutils provides gtimeout, which is the closest behavioural match to the GNU original.
- perl's alarm gives a dependency-free version that returns a distinguishable exit code, using an interpreter macOS already ships.
Questions
- Why has Apple never included timeout?
- The macOS userland derives from BSD, and timeout is part of GNU coreutils. It is not an omission so much as a different lineage. FreeBSD added its own timeout in 2014; macOS did not pick it up.
- Is gtimeout safe to rely on in a script?
- Only on machines where you control the environment. A script using gtimeout fails on any Mac without Homebrew coreutils, which is most of them.
- Does the perl version work if the command spawns children?
- Not reliably. Killing the parent leaves orphaned children in the general case. If your command spawns a process tree that matters, gtimeout with its --kill-after handling is the better choice.
Sources
What we read. Distinct from what we measured, which is in the article itself.