Timeouts and retries

Five options control how long a script may run and what happens when it fails. They belong on command and extends scripts, single or grouped. A group entry that declares one is a validation error.

Info

Flaky and slow scripts covers when to reach for each one. This page is the field reference.

OptionTypeDefaultMeaning
timeoutnumbernoneMilliseconds before the process tree is terminated
retriesnumber0Extra attempts after a failure
retryDelaynumber | 'exponential'0Milliseconds between attempts, or 2^attempt seconds. Requires retries
killSignal'SIGHUP' | 'SIGINT' | 'SIGQUIT' | 'SIGTERM' | 'SIGKILL''SIGTERM'The signal sent first when Rune terminates the script
killTimeoutnumber5000Milliseconds before escalating to SIGKILL

Every duration is a whole number of milliseconds.

timeout

'e2e': {
  command: 'playwright test',
  timeout: 600000,
}

When the budget elapses, Rune terminates the script's whole process tree through the same escalation path a group uses: killSignal, then SIGKILL after killTimeout. On Windows the job object is terminated, which takes the tree with it.

Rune then exits 124, matching GNU timeout, and prints the reason on stderr:

`e2e` exceeded its 600000 ms timeout — its process tree was terminated

A script that finishes inside its budget is unaffected. No early termination, no altered code.

On macOS and Linux a script that declares a timeout runs in a process group of its own, because that is the only way to reach its grandchildren. Rune forwards the terminal's interrupt to it, so Ctrl+C still ends it. A script with interactive: true keeps the terminal instead, and its timeout then reaches the process Rune started rather than the whole tree.

retries

'test:flaky': {
  command: 'vitest run tests/network',
  retries: 2,
  retryDelay: 'exponential',
}

Retries happen only on failure. A script that exits 0 runs once.

With retryDelay: 'exponential' the wait is 2^attempt seconds: 2 s before the second attempt, 4 s before the third. A number is a fixed wait in milliseconds.

Anything observing the script sees only the final attempt: the reported exit code, a group's failure handling, and a success policy. A script that fails twice and then succeeds is one success, not three events.

retryDelay without retries is refused when the config loads, because a wait before an attempt that will never happen is a mistake rather than a preference:

script `a` sets `retryDelay` but no `retries`

`retryDelay` is the wait between attempts, and without `retries` there is no second attempt
for it to come before.

timeout with retries

Each attempt gets a fresh timeout budget. A timed-out attempt counts as a retryable failure, and the timeout exit code surfaces only when the final attempt times out.

'deploy:smoke': {
  command: 'node scripts/smoke.mjs',
  timeout: 30000,
  retries: 3,
}

Four attempts of up to 30 seconds each, not 30 seconds in total.

killSignal and killTimeout

These describe how Rune terminates a script it decided to stop, whether from a timeout, a failing sibling in a parallel group, or a group teardown.

'dev:api': {
  command: 'node --watch server.js',
  killSignal: 'SIGINT',
  killTimeout: 2000,
}

A server that cleans up on SIGINT gets the chance to. If it has not exited after killTimeout, Rune sends SIGKILL.

Setting killSignal: 'SIGKILL' skips the timer, because there is nothing to escalate to. A process that had already exited is never an error.

On Windows there are no POSIX signals, and a job object is terminated or it is not. Rune ends the whole tree at once there, so killSignal and killTimeout are accepted — a config has to load on every machine — and have nothing to change. Both take effect on macOS and Linux.

What these do not cover

Ctrl+C from the terminal is not a Rune-initiated termination. The interrupt reaches the child directly and Rune waits for it, so killSignal and killTimeout play no part. See Exit codes.