Skip to content
vxvx
GitHubBlueskydev.toRSS
Blog

Where a task run spends its time

We traced one workspace through vx, Turborepo 2.11.8 and Nx 23.3.0, step by step, cold and warm. Most of a run is the tasks themselves. The rest is a list of choices, and some of vx's extra work is there on purpose.

A task tool does the same few things on every run: find the projects, build a graph, hash inputs, decide hit or miss, run or restore, record what happened. We traced each step in vx, Turborepo and Nx on one workspace to see where the time goes, and where vx does work the others skip.

The synthetic bench workspace: 1,601 projects, 8,002 tasks to run (9,603 graph nodes), 4 cores, concurrency 10, CI=1, local cache only, Linux container. Turborepo 2.11.8 with --profile traces; vx built from 9388bad8f with VX_DEBUG_TIMING=1; Nx 23.3.0 with nx:run-commands targets, daemon off as CI runs it, .env loading off. Measured 2026-10-13 and 2026-10-14. These are raw figures from one machine; the method and the scripts are on Benchmarks.

Cold
vx120.2 s
Turborepo 2.11.8127.3 s
Warm
vx892 ms
Turborepo 2.11.82,496 ms first, 909 ms second
One source file changed
vx100.1 s6,607 run
Turborepo 2.11.8105.8 s6,607 run
That edit reverted
vx3.6 s1,321 tasks restored, the rest left in place
Turborepo 2.11.85.2 sall 8,002 replayed
The table
RunvxTurborepo 2.11.8
Cold120.2 s127.3 s
Warm892 ms2,496 ms first, 909 ms second
One source file changed100.1 s (6,607 run)105.8 s (6,607 run)
That edit reverted3.6 s (1,321 tasks restored, the rest left in place)5.2 s (all 8,002 replayed)

On a cold run both tools spend about 97% of the wall time waiting on the commands they started. What the tool adds per task is small. We timed the bare commands in a shell (build 312 ms, lint 78 ms) and the same commands under vx: 314 ms and 78.4 ms. Spawn, environment and output capture come to about 0.4 ms a task. Turborepo’s trace shows about the same for its spawn.

Before the first task starts, vx spends about 640 ms finding projects, building the graph and listing files from git; Turborepo’s precompute is about 0.7 s.

Some of vx’s per-task cost is checks the others do not make:

  • Graph rules: two tasks writing one output, or a task reading another’s output without an edge, are refused. Only vx lock checks them; a run reads the lock and skips them.
  • Input recheck, 0.2 ms per miss: after a task exits, vx checks that its inputs did not move while it ran, and skips the save if one did (what four tools do).
  • Output clean, 0.15 ms per miss: declared outputs are cleared before a task runs, so a stale file cannot ride along into the cache.
  • Save before dependents, about 4.3 ms per build task: the save already runs off the task’s slot, in a save lane, but the task’s dependents wait for it to land, so they cannot rewrite the outputs while they are archived. On this graph that wait costs at most about 0.14 s of a 120 s run.
  • Remote upload at the end: uploads to a remote cache run in the background, a bounded number at once. Neither the task nor its dependents wait for one; the end of the run does, so every upload finishes before vx exits.
  • Blob-size check on a cold cache: once per git index, vx confirms that the blob IDs it reads from git match the files on disk, about 330 ms. After that it is remembered.

Each of these is a correctness guard. We measured them and kept them.

When every task hits, the commands never run and the tool’s own steps are the whole run.

StepvxTurborepo 2.11.8
Projects and graphdiscover, then graph with rulesprecompute
File hashesgit’s own index (63 ms)git index, then per-package file hashes
Keysone per taskone per task
On a hitchecks each task’s outputs on disk; restores a task’s outputs only if one differsrestores every output and log

The last row is the difference. Turborepo restores outputs on every hit unless an output watcher, which runs in its daemon, reports them unchanged; with no daemon, every hit rewrites its outputs. Source: crates/turborepo-run-cache/src/lib.rs, restore_outputs. In vx, if every output of a task still matches what vx saved (size, mode, modification time, and the inode and change time it recorded), the task’s outputs are left alone; otherwise that task’s outputs are restored.

Nx’s warm run on the same workspace took 21 to 24 s, all 9,603 tasks hit. A CPU profile of the main process shows where:

Self timeWhere
6.10 shashTasksUpfront: hands each task’s environment to the native hasher
5.55 sgarbage collection
3.03 sgetTaskSpecificEnv: copies process.env for each task
2.89 scopyFilesFromCache: writes every cached output back
1.56 splan, the native planner

getTaskSpecificEnv starts with { ...process.env } for every task, whether or not .env loading is on, so the cost grows with the size of the environment; this runner had 173 variables. Source: nx@23.3.0, dist/src/tasks-runner/task-env.js (getTaskSpecificEnv) and dist/src/tasks-runner/cache.js (copyFilesFromCache). We gave Nx the settings that helped it: run-commands targets (no process fork per task), daemon off (on was slower here), .env loading off.

Speed on a hit matters less than hitting when you should.

ChangevxTurborepo 2.11.8
Unrelated env var setall hitall hit
Root package.json editedall hitall 8,002 miss
One project’s package.json6,607 miss6,607 miss
bun.lock addedall missall miss

In our run, an edit to the root package.json changed every Turborepo task’s hash; vx sees root dependencies through the lockfile. A lockfile plugin such as bun() narrows the last row to the projects whose dependencies moved.

The trace also pointed at vx. We checked three candidates before calling anything waste. The output check on hits looked like 655 ms; measured alone it is 10.8 ms over 1,600 hits, and the larger figure was ten overlapping workers summed. When something outside vx rewrites a task’s outputs with identical bytes, vx restores that task’s outputs anyway, because the recorded times no longer match. That is real, but fixing it means a content hash on every save, which costs more than it saves. The wait for the save before dependents start is the 0.14 s above. None became a patch.

Run it yourself: the bench and its scripts are in the repository, and vx is at vznjs.github.io/vx.

Turborepo, Nx and other product names are trademarks of their owners. vx is not affiliated with or endorsed by them.