macOS does not ship a timeout command, so zsh: command not found: timeout is expected on a fresh Mac. The quickest fix with nothing to install is a one-line Perl wrapper, because Perl is built into macOS:
perl -e 'alarm shift; exec @ARGV' 5 your-command --args
That kills your-command after 5 seconds. If you want the real GNU timeout, install Homebrew’s coreutils package and use gtimeout. A third option is a small shell function you can paste into any script. All three are below, with the exit codes and gotchas I hit while testing.
Why macOS has no timeout command
timeout is part of GNU coreutils, the Linux toolset. macOS uses BSD-style tools instead, and Apple never added timeout. On my test Mac (MacBook Air M3, macOS Tahoe 26.6.2, zsh 5.9) nothing is installed by that name:
type timeout gtimeout
timeout not found
gtimeout not found
The only “time” tools in /usr/bin, /bin and /usr/sbin are things like time, uptime and a few DTrace scripts. None of them stop a command after a limit. Running a Linux script that uses timeout fails like this:
zsh:1: command not found: timeout
The exit code is 127, which means “command not found”. So a CI or build script will fail even if the command it wraps would have worked.
Method 1: perl alarm (no install)
This works on every Mac, because /usr/bin/perl comes with macOS (version 5.34.1 on Tahoe 26.6.2):
time perl -e 'alarm shift; exec @ARGV' 2 sleep 5
echo $?
perl -e 'alarm shift; exec @ARGV' 2 sleep 5 0.00s user 0.01s system 0% cpu 2.017 total
142
How it works: alarm shift takes the first argument (2) and sets a 2-second alarm. exec @ARGV then replaces Perl with your command, keeping the same process. When the alarm fires, the kernel sends SIGALRM and the command dies. It stopped after 2.017 seconds in my test.
If the command finishes in time, you get its own exit code and output untouched:
perl -e 'alarm shift; exec @ARGV' 2 echo hello # prints hello, exit 0
perl -e 'alarm shift; exec @ARGV' 2 ls /nonexistent # exit 1, ls's own error
To wrap a pipeline or several commands, pass them through sh -c:
perl -e 'alarm shift; exec @ARGV' 10 sh -c 'make && make test'
Gotcha: the exit code is 142, not 124
GNU timeout returns 124 when it kills a command. The Perl trick returns 142. That is 128 + 14, and signal 14 is SIGALRM:
kill -l 14
ALRM
If a script you copied from Linux checks if [ $? -eq 124 ], it will never match on a Mac with the Perl wrapper. Check for 142 instead, or use one of the methods below.
Gotcha: child processes keep running
This one surprised me. The alarm only kills the process Perl turned into. Anything that process started in the background survives:
perl -e 'alarm shift; exec @ARGV' 1 sh -c 'sleep 4 & echo child $!; wait'
pgrep -fl "sleep 4"
child 21570
21570 sleep 4
The sh was killed after 1 second, but its sleep 4 child was still running. For a simple command that is fine. For a test runner or server that spawns workers, you may end up with orphan processes. If something is left holding a port, see how to kill a process on a port.
Method 2: install coreutils and use gtimeout
If you use Homebrew, this gives you the real GNU tool:
brew install coreutils
gtimeout 5 your-command
I did not run the install on the test Mac, but brew info shows what you would get:
==> coreutils: stable 9.12 (bottled), HEAD
GNU File, Shell, and Text utilities
https://www.gnu.org/software/coreutils/
...
Not installed
Homebrew puts a g in front of every GNU tool, so you get gtimeout, gdate, gls and so on. That avoids clashing with the macOS versions. If you want plain timeout to work (for example, to run Linux scripts unchanged), coreutils also ships unprefixed names in a separate folder. On Apple Silicon that is /opt/homebrew/opt/coreutils/libexec/gnubin. Adding it to the front of your PATH makes all GNU tools win over the macOS ones, including ls, date and sed. That can break other scripts that expect BSD flags, so many people only add an alias instead:
alias timeout=gtimeout
If brew itself is not found, fix that first with zsh: command not found: brew. To remove coreutils later, see how to uninstall a Homebrew package.
Method 3: a zsh/bash timeout function
If you can’t install anything and you need GNU-style exit codes, paste this function into your script:
timeout() {
local secs=$1; shift
"$@" &
local pid=$!
( sleep "$secs" && kill -TERM "$pid" 2>/dev/null ) &
local watcher=$!
wait "$pid"
local rc=$?
pkill -P "$watcher" 2>/dev/null
kill "$watcher" 2>/dev/null
wait "$watcher" 2>/dev/null
(( rc == 143 )) && return 124
return $rc
}
It runs your command in the background, starts a “watcher” that sends SIGTERM after the limit, and maps the resulting 143 (128 + 15) to 124, like GNU timeout. My test in zsh:
exit=124 after 2s
exit=0
ls: /nonexistent: No such file or directory
exit=1
It also works in the old bash 3.2.57 that macOS ships, but bash prints a job message when the command is killed:
./tmo2.zsh: line 7: 21675 Terminated: 15 "$@"
bash exit=124
Gotcha: the pkill -P line matters
My first version had no pkill -P "$watcher" line. It returned the right exit codes, but pgrep showed leftover sleep 5 and sleep 3 processes after the commands finished early. Killing the watcher subshell does not kill the sleep running inside it. Each one sat idle until its time ran out. The pkill -P line kills the watcher’s children first. With it, pgrep printed nothing.
Which method should you use?
| Method | Install needed | Exit code on timeout | Signal sent |
|---|---|---|---|
perl -e 'alarm shift; exec @ARGV' | No | 142 | SIGALRM |
gtimeout (coreutils) | Homebrew | 124 | SIGTERM (change with -s) |
| Shell function above | No | 124 | SIGTERM |
For a one-off command in Terminal, use Perl. For scripts shared with Linux machines, install coreutils so behavior and flags match. For a script that must run on any Mac with no setup, use the function.
Common questions
Is there a built-in timeout command on macOS Tahoe?
No. I checked macOS Tahoe 26.6.2 and there is no timeout or gtimeout in /usr/bin, /bin or /usr/sbin. You need one of the three methods above.
Why does my script say “command not found: timeout” only on Mac?
The script was written for Linux, where timeout is part of coreutils and always present. On a Mac the shell can’t find it and returns exit code 127. Install coreutils and alias timeout to gtimeout, or define the function at the top of the script.
Can I make timeout send SIGKILL instead of SIGTERM?
With gtimeout, yes: gtimeout -s KILL 5 cmd, or gtimeout -k 2 5 cmd to send SIGKILL 2 seconds after the first signal if the command is still alive. In the shell function, change kill -TERM to kill -KILL and map 137 instead of 143.
Does the perl trick work for shell functions or aliases?
No. exec needs a real program on disk, so it can’t run a zsh function or alias. Wrap those in zsh -c '...', and remember that the new shell won’t load your aliases unless you source the file that defines them.
Will the timeout stop a command running with sudo?
Not reliably. A command running as root can’t be killed by your normal user, so put the timeout inside sudo instead, for example sudo gtimeout 5 cmd. I did not run sudo on the test Mac.