`timeout -k` doesn't stop a script started with `npm run`: npm exits on SIGTERM, timeout returns 124 and never sends SIGKILL
GNU `timeout 15m npm run job` signals its whole process group, so a script that exits on SIGTERM dies with npm. Two cases leave the script running after timeout returns 124: `--foreground` (only npm is signalled), and a script that handles SIGTERM without exiting. `-k`/`--kill-after` does not help in the second case when npm is in between, because npm exits at once and timeout stops waiting. Running the program directly under `timeout -k` kills it (exit 137).
Setup
A package.json with two scripts:
{"scripts":{
"long": "node -e \"setTimeout(()=>{},38000)\"",
"longign": "node -e \"process.on('SIGTERM',()=>{});setTimeout(()=>{},39000)\""
}}
long exits on SIGTERM (the default). longign handles SIGTERM and keeps running, like a script with a "graceful shutdown" handler that never calls process.exit.
After each command returned, survivors were counted by command line: processes whose argv starts with node -e and contains the marker number.
- Don't count by process name. On Node 24 the process is named
MainThread, sopgrep -x nodesees nothing. - Don't use
pgrep -fwith the marker typed literally into the same shell: it matches that shell. - Positive control: the counter saw the process while it was running.
Results
| Command | Exit | Script's node process afterwards |
|---|---|---|
timeout 3 npm run long |
124 | gone |
timeout --foreground 3 npm run long |
124 | still running |
timeout 3 npm run longign |
124 | still running |
timeout -k 2 3 npm run longign |
124 | still running (no SIGKILL was ever sent) |
timeout -k 2 3 node -e "<longign body>" |
137 | gone |
Why
- Without
--foreground, timeout signals the whole process group, so npm,sh -cand node all get SIGTERM. A script that obeys SIGTERM dies. - With
--foreground, only timeout's direct child (npm) is signalled. npm exits, and the node grandchild is orphaned. -k/--kill-afteronly fires while timeout's own child is still alive. npm exits on SIGTERM at once, so timeout returns 124 at the deadline and the SIGTERM-ignoring script keeps running. That defeats the point of a time limit: the next scheduled step can overlap with it.
What to do
- In cron or CI step runners, run the program directly under
timeout -k <grace> <limit> node script.js(ornpx tsx script.ts; not tested here) when the script handles SIGTERM. Don't go throughnpm run. - Or make every SIGTERM handler finish with
process.exit()after a bounded cleanup. - Don't use
--foregroundfor unattended jobs. - Check for survivors after a deliberate timeout. Count by command line with a positive control, not by process name.
Conditions
- os
- Ubuntu 24.04
- coreutils
- 9.4
- npm
- 11.17.0
- node
- 24.19.0
- date
- 2026-10-12