Skip to content
GitHubRSS

Why vx is fast: five decisions, not a trick

The headline number is the one you pay on every uncached build: what the runner adds on top of your tasks. On a synthetic workspace of 1,090 packages and 3,270 tasks whose ideal schedule is 3m 38s, vx finishes the cold build in 3m 46s, eight seconds over the schedule. Turborepo finishes in 5m 13s (a minute and a half over) and Nx in 34m 44s (half an hour over). Warm, a fully cached vx run build test --all finishes in 510ms, Turborepo in 760ms and Nx in 3.59s; the cold build burns 35 s of CPU in vx, 73 s in Turborepo and 114 minutes in Nx. On a real Turbo repository (solidjs/solid) the warm restore is 66 ms against Turbo’s 127 ms.

None of that comes from a microbenchmark trick. It comes from five decisions, and every one of them is also a reason to trust the cache more, not less.

1. The cache key is already in git’s index

Section titled “1. The cache key is already in git’s index”

A content-addressed key needs a hash of every input file. Most tools read each file and hash it, or keep a daemon around so they do not have to. vx spawns one git ls-files -s, which returns the file list and every clean file’s blob object id, and one concurrent git status to prune anything that diverges from the index. Clean-tree key derivation costs zero file reads, zero stats and zero database lookups.

Dirty files get the identical blob id computed in-process, so a key never flips when you commit. That class of spurious miss, “I committed and everything rebuilt”, does not exist here.

There is a whole post on this: Your cache key is already in git’s index.

Scheduling priority and the package graph are computed over packed bitsets with popcount instead of set-union depth-first search. On the 3,270-task graph that turned an 8.5 s priority computation into single-digit milliseconds. The scheduler tick re-scans nothing: ready tasks come off an exact most-blocked-first binary heap, a completion decrements its direct dependents’ counters and pushes the ones that reach zero, and the run costs one pass over the edges plus an O(log N) heap operation per task.

3. Strict output ownership makes restore cheap

Section titled “3. Strict output ownership makes restore cheap”

Declared outputs are wiped before a miss executes and before a hit restores, so the tree after either is exactly the cached snapshot. That is a correctness rule first (no stale dist/old.js survives), but it also means vx knows what the tree looks like after a hit. On a warm-on-warm run it verifies the recorded (size, mode, mtime, inode, ctime) of each output with a stat and writes nothing, decompresses nothing. A restore onto a current tree costs about what an untouched tree costs.

A cache entry is one tar.zst archive plus a SQLite row. Metadata and the captured stdout live in the row, so a hit is one indexed SELECT and a replay from the row, not a decompression. The same bytes go over the wire to a remote cache; nothing is repacked at the boundary. Packing is in-process (vx’s own streaming tar), the publish is an atomic rename, and each save is a single transaction.

vx has no daemon, so there is no process to keep warm and no staleness window between what the daemon believes and what the disk holds. What replaces the daemon is a set of zero-cost gates: no telemetry plugin means no event-bus subscriber, no summary, no git spawn for provenance; a plugin that declines a task costs nothing; a config that passes the purity gate is served from an evaluation cache keyed on the git blob ids of its whole import closure, and a config that reads the environment or the clock is simply evaluated live.

Every change to the warm path in this repository ships with a number, measured the same way: A/B arms interleaved, min-of-N, the “before” arm checked out into an immutable git worktree, one workspace copy per arm pre-warmed by that arm. A change without a number is not done. The history of where the headroom went is in Benchmarks, and the full catalogue of decisions, each with its invariant and its source file, is in Optimizations.