Tuning tman for Vitest
Watch mode never exits; the thread pool is where memory goes.
Recommended starting caps
| cap | start at | what it answers |
|---|---|---|
stall | 10m | hang backstop — no output and no CPU/IO activity for this long |
max-time | 15m | wall-clock budget — the only cap that bounds a run that is working but will never finish |
max-mem | 4096 | RSS ceiling across the process tree |
max-parallel | 2 | slots per name-and-directory bucket |
These are starting points derived from how Vitest behaves, not measurements of your machine. Every one of them should be replaced by a number you took from your own runs — the last section on this page is how.
alias "test" {
command "npx"
args "vitest" "run"
max-time "15m"
max-mem 4096
}Vitest streams results as they land, so output-based stall detection sees it constantly and the backstop rarely has to reason about anything. The two things worth configuring are the mode and the memory ceiling.
Always `vitest run`, never bare `vitest`
Bare vitest enters watch mode and stays resident. Under an agent that is a durable problem: the agent gets no exit code, moves on, and leaves a file watcher and a worker pool alive for the rest of the session. The alias should pin run so the mode cannot be inherited from a config file or a habit.
Never put a wall-clock cap on
vitest --watch. It is designed not to exit, somax-timeguarantees a kill and an exit 124 that means nothing. Use--nameinstead: the dedup lock is what you actually want, because it stops an agent starting a second copy on top of the first.
Where the memory goes
The default pool forks worker threads, and a suite that mounts a DOM per test — jsdom or happy-dom — accumulates across a run when teardown is incomplete. A max-mem of 4 GB is high enough that an ordinary suite never sees it and low enough that a leak is caught before the machine starts swapping. On Linux the ceiling is summed across the whole process tree, so it covers the workers, not just the parent.
If you hit 126 repeatedly, cap the pool before you raise the ceiling: --pool=threads --poolOptions.threads.maxThreads=4 bounds the multiplier that turns a small per-worker leak into an OOM.
Sizing the stall backstop
Vitest's quietest stretch is the first transform of a cold dependency graph — a large monorepo can sit for a minute or two before the first test result appears. 10m leaves an order of magnitude of headroom over that while still catching a wedged worker in a coffee break rather than a working day.
Turn the starting point into a real number
Everything above is derived from how this tool behaves, not from your machine. The numbers that matter come from your own runs, and getting them takes one command:
tman status --json | jq '.runs[] | {name, exit, duration_ms}'Set max-time from the slowest successful run you are willing to wait for, with real headroom — a cap that fires on healthy work teaches everyone to raise the cap, and after a few rounds of that it bounds nothing. Set stall from the longest stretch the command legitimately produces no output and no CPU, then double it.
Exit 124, 125, and 126 are findings, not flakes. 124 is a wall-clock timeout, 125 a stall, 126 a resource cull. Widening the cap that fired is the right response only after you know why it fired — the first two times a suite trips
max-mem, the interesting question is what is holding the memory.
Frequently asked
What tman caps should I use for Vitest?
Start with max-time 15m, stall 10m, max-mem 4096, max-parallel 2, and pin `vitest run` in the alias so watch mode can never be inherited. Adjust max-time from your own slowest successful run.
Why does my Vitest run get killed with exit 126?
126 is a resource cull — the process tree crossed max-mem or sustained max-cpu. With Vitest that usually means a worker pool accumulating DOM instances across tests. Cap the pool size before raising the ceiling; a ceiling raised to fit a leak stops being a ceiling.