Why vx is fast — and what it does that others don't
The condensed story of every structural advantage, with measured
numbers. Mechanics live in optimizations.md;
history in CLAUDE.md’s decision log; raw numbers in
benchmarks.md.
Headline numbers (same machine, same workspaces)
Section titled “Headline numbers (same machine, same workspaces)”- 100-project comparison repo, warm: vx 144 ms · Turbo 279 ms · Nx 583+ ms (wall-clock, direct binaries). vx’s restore costs the same as an intact tree.
- 1090-package / 100-layer stress repo, fully cached: 10.2 s → 0.62 s across the June 2026 scaling pass.
- 500 projects × 30 files, warm run-phase: 245 ms → 76 ms (3.2×) after git-OID hashing.
Differentiators — things Turbo and Nx do not have
Section titled “Differentiators — things Turbo and Nx do not have”- Resolved-config hashing. The cache key sees the
post-evaluation
vx.config.tsobject — imports, presets, and computed values participate. Turbo/Nx hash the static config file and miss them. - Strict output ownership. Declared outputs are wiped before exec AND before restore, so the tree ends every run bit-identical to the cached snapshot — no stale stragglers from a prior build or a hand edit. Turbo/Nx restore additively; removed files survive.
- Sparse
^taskbridging.^buildwalks through dep packages that don’t declare the task to the nearest holders (Turbo/Nx stop at direct deps). Since v19 this is a frontier walk — Turbo-grade edge counts with the bridging kept. - Restore-exact gitignore semantics with zero re-spawns. After a restore, downstream tasks re-enumerate via git only when their input globs can actually see a changed path (920 ms → 136 ms on the repo that reported it; 81 git spawns → 1).
- Always-on artifact integrity — no key management. Every
artifact on the vx-native cache wire carries an
x-vx-digest(xxh3 over the bytes) the client verifies against the received body, so a corrupt store or truncating transport degrades to re-execution, never a restored artifact — structural, not opt-in (Turbo’s HMAC signing needs a shared key on every machine). The server additionally refuses junk uploads (a non-zstd body 400s) so an accidental bad PUT can never lock a key in the immutable store. - A committed config lockfile.
vx lockfreezes every project’s resolved config (post-evaluation objects — computed values and config-time env reads included) intovx-lock.json; CI audits it with a full re-evaluation (vx lock --check) and loads exactly it (--frozen) with zero config evaluation. The graph is still built per run, and live-resolved inputs (inputs.envvalues,runtimecommand output) stay live under--frozen. Local runs always evaluate live. Turbo and Nx have no equivalent — their static-JSON configs dodge the problem by being less expressive; vx keeps code-as-config AND reproducibility. - Daemonless by design. No background process, no staleness window,
no socket state — and the fastest warm/cached runs in the committed
head-to-head benchmark (
bench/compare.ts). - Runtime inputs.
cache.inputs.runtime/workspaceRuntimefold a probe command’s live output into the key (tool versions, OS info) — deduped per run, correct under--frozen. Nx has aruntimeinput; Turbo has none (vercel/turborepo#4124). - Restore-ahead two-tier scheduling. A stable-key warm hit is knowable up front, so it restores immediately — before its deps finish running — as low-priority worker backfill (misses own the pool). Measured −6.6% on mixed workloads, parity on all-warm. Remote runs get the same idea as async prefetch: stable-key remote GETs fire in the background and overlap execution, at most one per key; uploads are backgrounded too and drained at run end.
- A versioned telemetry contract + run-level plugins.
TelemetryRecord/RunSummaryRecordis one neutral, versioned export shape every sink reads (OTel via@vzn/vx-otel, a self-hosted dashboard, custom sinks) — observe-only by construction, crash-isolated, and provably zero-overhead when no sink is active. - Client/server environments. The first-party cloud plugin’s
connect/envverbs give docker-context-style named servers with bearer auth; every developer’s and CI’s runs aggregate in one dashboard. See the Cloud section of the docs.
Note: an earlier design folded the upstream’s output content identity into downstream keys (“early cutoff”, v21). It was reverted in v22 in favor of pure-input transitive hashing (
upstream.tsfolds the upstream’s input key, like Turbo/Nx). An upstream that re-runs but emits identical output therefore still re-runs its dependents —caching.mdis the authority.
Perf tricks under the hood
Section titled “Perf tricks under the hood”- Hashes come from git’s index (v20, Turbo technique, extended).
git ls-files -syields every clean file’s blob OID in the same spawn that enumerates files;git status(run CONCURRENTLY) prunes divergent paths; dirty files get byte-identical blob OIDs in-process so a key never flips across commit boundaries — a spurious-miss class Turbo accepts. Clean-tree hashing costs zero reads, zero stats, zero DB lookups. - Bitset graph algorithms everywhere a closure is needed (scheduler priority, package graph): exact counts via popcount, O(E·N/32), replacing Set-union DFS that cost 8.5 s at 3270 tasks.
- One bulk git enumeration per run, partitioned per project by binary search over the sorted file list.
- Stat-check restore skip: warm-warm restores are N stats, zero
writes, zero decompress (
output_filesfingerprints in SQLite). - v17 single-format artifacts: local and remote transport identical tar.zst bytes; metadata rides SQLite/HTTP headers — no repack anywhere.
- xxh3 key derivation, seed-chained,
\0-delimited part boundaries (collision-hardened in v18). - In-process tar (15-400× faster than Bun.Archive at our sizes), atomic tmp+rename publish, single-transaction batch SQL.
- O(N+E) scheduler tick with an exact most-blocked-first priority queue — now two-tier (exec queue + restore-backfill queue).
- Pure-SQL cache hits: metadata + stdout live in the
entriesrow, so a hit never decompresses the artifact (118 ms → 5 ms on large-binary entries);accessed_atbumps batch into one UPDATE. - Remote latency off the critical path: prefetch overlaps GETs
with execution; uploads are fire-and-forget background tasks drained
at run end;
--dry/--graphprobe remote existence without downloading.
Deliberately rejected (with receipts)
Section titled “Deliberately rejected (with receipts)”- Config-eval cache / project-graph cache: configs are programs; soundness needs a correctness-critical purity heuristic for ~200 ms at 1000 projects. Re-evaluate only with a sound dependency story (see benchmarks.md headroom).
- fspy-style auto-input tracing: a ~9-crate native systems project per vite-task’s implementation (seccomp_unotify, Detours, shipped shell for macOS SIP). Incompatible with no-build-step distribution (see comparison.md §3).
- Cross-task artifact sharing / CAS dedup: storage-only win; identical outputs are rare in real builds (path embedding).
- Nx-style daemon: the cold-start numbers say we don’t need one.