vx, Turborepo, Nx, Bazel
Four tools run the tasks of a monorepo and cache their results: vx, Turborepo, Nx and Bazel. All four build a task graph, compute a cache key per task and share a remote cache. A feature table ticks those boxes for every tool and stops there. It does not say how the key is computed, what checks it, who hosts the cache, or what each answer costs you.
This page names the design choices behind the four tools. For each one it says what each tool chose, what that buys, what it costs, and when another tool’s choice is the better one. After reading it you should be able to choose a tool for stated reasons, including when that tool is not vx.
vx’s maintainers wrote this page. Every claim about another tool links to that tool’s own documentation, checked against its docs source on 2026-09-24. Every claim about vx links to the page or the test that holds it. When a tool’s docs say nothing about something, this page says “its docs describe no …”. That is not the same as “it cannot”.
A choice, not a checkmark
Section titled “A choice, not a checkmark”Take the first question every runner answers: which files can change a task’s result? Those files are the task’s inputs, and a runner can find them three ways.
- A default. Hash every file in the project. Nothing to write, and a task reruns whenever anything in its project changes, even a README.
- Inference. Read the configuration of the tools a task runs, such as
vite.config.ts, and derive the inputs from it. Little to write, as long as the inference is right. - Declaration. You list the inputs. The key covers exactly what you wrote, and the lists are yours to keep right.
None of the three is free. A file the key does not cover can make a stale hit: the cache replays yesterday’s result under a green run. So a second choice sits beside the first: does anything check that the key covers what the task reads? A sandbox does, by running the task where undeclared files cannot be reached.
The two choices together decide what a cache hit is worth, and how much work a task takes to set up:
Turborepo hashes every tracked file in the package by default
(inputs),
and its docs describe no file sandbox. Nx starts from every file in the
project and lets plugins infer more
(inputs), and it sandboxes tasks
on Nx Cloud (task sandboxing).
Bazel makes you declare every action’s inputs and sandboxes each action
by default (sandboxing). vx makes
you declare the inputs and sandboxes the tasks that opt in
(Sandboxing tasks).
The other choices
Section titled “The other choices”The same trade shows up everywhere else. The matrix below has twelve choices in four groups:
- Correctness. How a task’s inputs are found, and what checks the key.
- Writing it down. The configuration language, how the runner is extended, and which languages it builds.
- Where the work runs. The runtime the runner needs, the remote cache wire, running tasks on other machines, which ready task starts first, and whether a process keeps state between runs.
- Getting there. Coming from another tool, and how mature each tool and its ecosystem are.
Filter by what you need
Section titled “Filter by what you need”The first table lists needs a team might have and which tools meet each. With JavaScript on, tick the ones that matter to you: the second table keeps only the choices that decide them, marks the tools each favours, and says which need rules out each tool. Without JavaScript, both tables show everything.
| Tick | Need | vx | Turborepo | Nx | Bazel |
|---|---|---|---|---|---|
| Bun is not allowed in our toolchainvx runs its configs and plugins on Bun, installed or inside its binary. Turborepo is a native binary, Nx runs on Node and Bazel on its own JVM.Decided by What the runner runs on | no | fits | fits | fits | |
| We build on native Windows, not WSLTurborepo ships Windows binaries, Nx installs on Windows through npm or Chocolatey, and Bazel documents a Windows install. vx runs on Windows only under WSL.Decided by What the runner runs on | no | fits | fits | fits | |
| Undeclared inputs must be caught on our own machinesvx and Bazel sandbox tasks locally. Turborepo has no file sandbox, and Nx sandboxes only on Nx Cloud.Decided by How a task's inputs are found, What checks the key | fits | no | no | fits | |
| Caching must work before anyone writes input listsTurborepo and Nx default to every file in the project. vx and Bazel make you declare inputs.Decided by How a task's inputs are found | no | fits | fits | no | |
| Send tasks to a build farm over an open protocolvx and Bazel speak the same open remote execution protocol. Nx Agents distribute CI tasks on Nx Cloud, and Turborepo documents no remote execution.Decided by Running tasks on other machines | fits | no | no | fits | |
| Try it on our Turborepo or Nx repository with nothing rewrittenturbo() and nx() run the existing configuration as it is. nx init writes a new nx.json, and the others need new configuration.Decided by Coming from another tool | fits | no | no | no | |
| Other languages are first-class, with their own dependency graphsBazel builds any language through rules, and Nx has stable Gradle, Maven and .NET plugins. Turborepo’s native Go, Cargo and uv support is experimental, and vx is for JavaScript.Decided by What it builds | no | no | fits | fits | |
| A stable release with a published support policyTurborepo, Nx and Bazel publish support windows. vx is pre-alpha.Decided by Maturity and ecosystem | no | fits | fits | fits | |
| Extend the runner with plugins written in TypeScriptvx and Nx plugins are TypeScript. Bazel is extended in Starlark, and Turborepo documents no plugin API.Decided by How the runner is extended | fits | no | fits | no |
| Tool | What it chose | What that buys | What it costs |
|---|---|---|---|
| How a task's inputs are foundDoes the runner hash every file it can see by default, work out the inputs from your tools, or make you declare them? | |||
| vx | Declared. A cached task must list its input files, and caching is off until it does. Nothing is inferred.
| The key covers exactly what the config says, so a miss can be explained file by file (vx why). | Writing and maintaining the lists is your work. A file a task reads but the list leaves out is a stale hit waiting to happen, unless the sandbox catches it. |
| Turborepo | A default. With no inputs key, a task's inputs are all files in the package checked into source control, plus package.json, turbo.json and the lockfiles. An inputs list narrows it. | Caching works before anyone writes an input list. | An edit to any tracked file in the package reruns every task there. A file outside the package needs globalDependencies or $TURBO_ROOT$. |
| Nx | A default and inference. By default a task's inputs are all files in the project and in the projects it depends on. Plugins infer inputs from tool config such as vite.config.ts. | Little to write, and by default Nx reruns a task when it should. | Its docs say the default may rerun tasks when irrelevant files change. Settings come from plugins, targetDefaults and the project, merged, so nx show project is how you see the result. |
| Bazel | Declared, for every action. BUILD files list each target’s sources, dependencies, data and tools. | A complete action graph: enough to sandbox every action and to run it on another machine. | Every dependency is written down, for every package, and kept current by hand or by tools such as Gazelle. |
| Choose another tool if |
| ||
| What checks the keyWhen a task reads a file its key does not cover, does anything notice, and where does that check run? | |||
| vx | A local sandbox, opt-in per task (exec.sandbox), on Linux and macOS. A read or write outside the grants is denied, and the task fails.
| An undeclared read turns into a failed task on your own machine and in CI, before it becomes a stale hit. | It is off until a task opts in. Its grants are a second list next to the inputs, and it proves the key only as far as the two agree. No Windows. |
| Turborepo | Its docs describe no file sandbox. Strict Mode, the default, passes a task only the environment variables turbo.json names. | Nothing to set up, and a task that needs an undeclared variable is likely to fail. | Its docs warn that Strict Mode does not guarantee a failure, and describe nothing that checks the files a task reads. |
| Nx | Task sandboxing, an Nx Cloud add-on that runs on Nx Agents in a dedicated compute cluster. Warning mode reports violations; Strict mode fails the task. | A record of every file each task touched, with a dashboard of violations across the organisation. | It needs the add-on, the cluster and Nx 22.6 or later, and it is not supported when you run the agents on your own compute. |
| Bazel | A local sandbox, on by default where the system supports it: each action runs in a directory holding only its declared inputs. | Every action is checked on every build, with no opt-in, and a build that works sandboxed is likely to work remotely. | Its docs say a tool that knows an absolute path can still read a file outside the sandbox. Every input must be declared first. |
| Choose another tool if |
| ||
| The configuration languageIs the configuration data that any tool can read, or a program that is run? | |||
| vx | TypeScript: a vx.config.ts per package and a vx.workspace.ts. vx evaluates each file and hashes the object it returns.
| Types and autocomplete, shared presets as plain imports, and computed values that still reach the key. | A config is a program: it must be evaluated (vx caches configs it can prove pure), it runs on Bun, and other tools cannot read it as data. |
| Turborepo | JSON: one turbo.json at the root (turbo.jsonc for comments), and per-package turbo.json files that extend it. | Static data with a schema, readable by any tool without running code. | No computation: reuse goes through extends and microsyntax such as $TURBO_DEFAULT$ and $TURBO_EXTENDS$. |
| Nx | JSON: nx.json for the workspace, and project settings in package.json or project.json, merged with what plugins infer. | Little to write per project when plugins infer the tasks. | Three sources are merged in order (plugins, targetDefaults, the project), so the effective settings are not in any one file. |
| Bazel | Starlark, a language with Python-inspired syntax: a BUILD file in every package, and .bzl files for macros and rules. | A real language for the build, one syntax for every language the repo holds. | A language and a set of files your team must learn and keep beside the ones each tool already has. |
| Choose another tool if |
| ||
| How the runner is extendedCan outside code change how the runner works, and at which points? | |||
| vx | Plugin stages, from config to telemetry. Core applies no plugin by default; running and caching on this machine are the floor under every plugin.
| Remote caches, remote execution, Turbo and Nx adoption and telemetry are all plugins, built without forking core. | A young API with few plugins besides the first-party ones. Plugins run on Bun, and none can turn a task into anything but a command. |
| Turborepo | A fixed core configured by turbo.json. Its docs describe no plugin API; they describe Turborepo as lightweight, with your repository as the source of truth. | Nothing Turborepo-specific in your packages beyond the config. | What the core does not do, you do in scripts, or wait for a release. |
| Nx | Plugins written in TypeScript that add project graph data, inferred tasks, generators, migrations and executors, from Nx and the community. | Knowledge of a tool (Vite, Jest, Gradle) packaged once instead of written in every project. | The settings a plugin infers have to be inspected and overridden when they are wrong. |
| Bazel | Rules and macros written in Starlark. A rule defines the actions Bazel runs on its inputs to produce outputs. | Full control over the actions for any language or tool. | Writing rules is its own skill. Bazel’s JavaScript rules, such as rules_js, are maintained outside the Bazel repository. |
| Choose another tool if |
| ||
| What the runner runs onWhich runtime must every machine have to run the tool, its config and its plugins? | |||
| vx | Bun. The npm package runs a binary with Bun inside, for Linux and macOS on x64 and arm64; from source it needs Bun 1.4 or later. Configs and plugins run on Bun. | Bun’s SQLite, process spawning, globbing and compression are built in, and one binary carries all of it. | A config or plugin that needs an API only Node has does not run. No native Windows: Windows runs vx under WSL. |
| Turborepo | A native binary, shipped through npm for macOS, Linux and Windows on x64 and arm64. Core turbo does not depend on your Node version. | Native Windows, and no JavaScript runtime needed to run tasks. | Some packages around it, such as create-turbo and turbo-ignore, do need Node, on its LTS versions. |
| Nx | Node.js: Nx is an npm package, loads TypeScript config through Node, and installs through npm, Homebrew, Chocolatey or apt. | It runs wherever your JavaScript toolchain already runs, Windows included. | Nx itself and its plugins start as Node processes on every command. |
| Bazel | A client and a long-lived server that runs in a JVM. The bazel executable bundles its JDK. Linux, macOS and Windows. | Independent of any language’s toolchain: the server is the same for every repository. | A JVM server per workspace and user, and your team has one more runtime to reason about. |
| Choose another tool if |
| ||
| What it buildsDoes the runner understand projects in other languages, with their own dependency graphs? | |||
| vx | JavaScript monorepos. A project is a package-manager workspace package. A task is any shell command, so it can call go or cargo, but vx reads no go.mod or Cargo.toml. | One model, with no plugin needed to find projects. | Another language’s dependency graph is invisible unless you wrap each project in a package.json. Non-JavaScript project support is out of scope. |
| Turborepo | JavaScript workspaces, plus experimental native Go, Cargo and uv workspaces. Other languages need a package.json beside each project. | Go modules and Rust crates in the same graph as JavaScript packages, without package.json files. | The native support is experimental and can change at any time. |
| Nx | Any language: a project.json target runs any command, and first-party plugins read Gradle, Maven and .NET builds into the graph. Python, Go and Rust plugins come from the community. | JVM and .NET projects in the same graph as JavaScript, with their dependencies. | Outside those, coverage depends on community plugins. |
| Bazel | Any language, through rules. Bazel builds projects in multiple languages for multiple platforms. | C++, Java, Go, Rust and more in one graph, with one set of guarantees. | Each language comes with its own rules to learn and maintain. |
| Choose another tool if |
| ||
| The remote cache wireHow does a cache reach other machines, and who hosts it? | |||
| vx | No wire in core: a plugin brings it. @vzn/vx-reapi speaks Bazel’s remote cache protocol; turboCache() speaks Turborepo’s, nxCache() speaks Nx’s. A remote error becomes a miss.
| A cache server you already run for Bazel, Turborepo or Nx serves vx too. | No first-party hosted cache: you run or rent someone else’s server. The plugins are not on npm until 0.1.0 is cut. |
| Turborepo | Its own HTTP API. Vercel Remote Cache is free on all plans, or you self-host a server that follows the published OpenAPI spec. | A hosted cache with one login, or a small server of your own. | turbo login and a Vercel account for the hosted cache. |
| Nx | Nx Cloud as the managed remote cache, or a server you build from Nx’s OpenAPI spec. | A managed cache next to Nx Cloud’s other CI features. | The managed cache means an Nx Cloud workspace; regional and self-hosted deployments are Enterprise options. |
| Bazel | Open protocols: HTTP/1.1 (any server that supports PUT and GET) and gRPC, plus a local disk cache. | Many servers speak it, open source and commercial. | You choose, run or buy the server. |
| Choose another tool if |
| ||
| Running tasks on other machinesCan one run send its tasks to other machines, and over what? | |||
| vx | Remote execution through @vzn/vx-reapi, over Bazel’s open protocol (REAPI), to worker pools such as NativeLink, BuildBuddy or Buildfarm. The scheduler stays on your machine. Off by default.
| A laptop or a CI job can use a build farm, one task at a time, with the rest of the run unchanged. | Every input must be declared, since the worker sees nothing else. vx ships no workers, service or dashboard: you run a pool or rent one. |
| Turborepo | None in its docs. Its CI guide speeds a run up through parallelism and the remote cache, which shares results between machines. | Nothing to operate beyond the cache. | Its docs describe no way to spread one run’s tasks across machines. |
| Nx | Nx Agents on Nx Cloud: CI tasks are distributed across machines by their past run times and dependencies. | Distribution with one line of CI config and agents sized to each PR. | It is an Nx Cloud service, billed in credits beyond each plan’s allowance. Running the agents on your own CI machines is an Enterprise plan feature. |
| Bazel | Remote execution over an open gRPC protocol (remote-apis), with self-hosted and commercial services. | Actions spread across a datacenter, one execution environment for the team, and shared outputs. | Rules must be adapted for remote execution, and someone runs or pays for the service. |
| Choose another tool if |
| ||
| Which ready task starts firstWhen more tasks are ready than there are workers, how does the runner pick? | |||
| vx | By default, the task with the most tasks waiting on it. @vzn/vx-schedule-history ranks by the remaining critical path learned from past runs. Cache restores get their own lane. | A documented order that works on the first run, and a better one once there is history. | The default cannot see that a task is long. The plugin needs history and must be declared; it runs on one machine. |
| Turborepo | --concurrency, 10 by default. Its docs do not say which ready task starts first. | One number to tune. | The order is not something you can read or change. |
| Nx | --parallel, 3 by default. On Nx Cloud, Nx Agents place tasks by past run times and dependencies. | History-aware placement across CI machines without configuring it. | Its docs do not say how a local run orders ready tasks. |
| Bazel | --jobs, and a scheduler that holds back actions whose estimated memory or CPU would not fit. | A machine is not overloaded by heavy actions started together. | Its docs call the estimates very crude, and do not say how ready actions are ordered. |
| Choose another tool if |
| ||
| State between runsDoes a background process keep the workspace in memory, or does every run start fresh? | |||
| vx | No daemon. Every run discovers the workspace and asks git what changed. | Nothing to keep in sync or reset. On medusa, run through turbo() unchanged, a warm run with nothing to do took 947 ms against Turborepo’s 3.29 s. | Every run pays for discovery and the git walk: on payload, 256 ms against Turborepo’s 237 ms. |
| Turborepo | No daemon for turbo run: its docs say the daemon is no longer used there and still serves turbo watch and the LSP. | The same as vx: a run reads the repository as it is. | The same: every run does its own discovery. |
| Nx | The Nx Daemon, on by default on developer machines: it watches files and keeps the project graph in memory. | Later commands get an up-to-date graph without starting the analysis from scratch. | A background process per workspace; nx reset stops it and clears the workspace state. |
| Bazel | A long-lived server that caches BUILD files, dependency graphs and other metadata between builds. | Fast incremental builds and queries that share one cache of loaded packages. | One invocation per server at a time: others block or fail. vx has not measured Bazel. |
| Choose another tool if |
| ||
| Coming from another toolWhat does it take to try the tool on a repository that already uses another one? | |||
| vx | turbo() runs a turbo.json repository unchanged, and nx() runs an Nx workspace from its resolved graph. bunx @vzn/vx-migrate writes vx.config.ts files when you want to move for good.
| A trial with one vx.workspace.ts and nothing rewritten. | The plugins are not on npm until 0.1.0 is cut. nx() runs Nx executors through a Node bin. Moving to native configs means writing input lists. |
| Turborepo | Incremental adoption on package-manager workspaces: install turbo, add a turbo.json. A guide covers moving from Nx. | Your scripts stay as they are, one task at a time. | Coming from Nx, the Nx configuration and plugins are replaced by hand. |
| Nx | nx init adds Nx to any repository. On a Turborepo repository it reads turbo.json and writes the equivalent nx.json. | One command, and your package.json scripts keep running through your package manager. | Some settings do not carry over, such as per-package task entries, and are ported by hand. |
| Bazel | A BUILD file for every package. Its docs have migration guides for Maven and Xcode; JavaScript goes through rulesets such as rules_js. | Once done, everything Bazel guarantees applies. | The migration is the work: every target and dependency written as Bazel rules. |
| Choose another tool if |
| ||
| Maturity and ecosystemIs there a stable release, a support policy, and an ecosystem around it? | |||
| vx | Pre-alpha. @vzn/vx is on npm as 0.0.x builds, 0.1.0 is not cut, and the plugins are not published yet. | Nothing yet, beyond the design: the API can still change where it is wrong. | No stable release, no support policy, a small ecosystem (the first-party plugins), and nothing distributed ships in the repository: no cloud, no workers, no dashboard. |
| Turborepo | Stable 2.x since June 2024. A major version is supported for two years after the next one ships; experimental features are marked. | A support window you can plan around. | Features marked experimental can change at any time. |
| Nx | A major release every six months; the previous major gets 12 months of LTS, 18 months of support in all. Official and community plugins. | Predictable releases, and nx migrate to move between them. | A major every six months to keep up with. |
| Bazel | LTS releases: each major is an LTS release, with rolling releases in between. Modules come from the Bazel Central Registry. | Years of support per major, and a registry of modules. | Rolling releases between majors can change behaviour; LTS is the conservative track. |
| Choose another tool if |
| ||
What vx’s choices cost
Section titled “What vx’s choices cost”- Bun only. vx runs its configs and plugins on Bun: Bun 1.4 or later from source, or the Bun inside its binary. A config or plugin that needs an API only Node has does not run, and Windows means WSL (Quickstart).
- Pre-alpha.
@vzn/vxis on npm only as 0.0.x builds. 0.1.0 is not cut, there is no support policy, and the plugins,turbo(),nx()and the remote ones among them, are not published yet (Roadmap to 1.0). - Explicit inputs are work. Every cached task lists its inputs, and
keeping the lists right is on you. The sandbox checks reads against its
own
allow.read, not the inputs, so it catches what they miss only on the tasks that opt in and whoseallow.readmirrors their inputs (Caching). - A small ecosystem. The plugins that exist are the first-party ones (Writing a vx plugin).
- Nothing distributed ships. The repository holds no workers, no service and no dashboard. Remote caching and remote execution are plugins that speak other tools’ protocols to servers you run or rent (Writing a vx plugin).
- No first-party cloud. Nobody sells a hosted vx (Remote caching).
When another tool is the better choice
Section titled “When another tool is the better choice”- Turborepo, if caching should work before anyone writes an input
list (
inputs), if you build on native Windows, or if you need a stable release with a support policy today (support policy). - Nx, if you want plugins that already know your tools (Nx plugins), JVM or .NET projects in the same graph (multi-language support), or CI distribution run for you as a service (Nx Agents).
- Bazel, if the repository is heavily polyglot (intro), if every action should be sandboxed by default (sandboxing), or if you need remote execution in production today (remote execution).
vx is the pick when you want declared inputs and a local sandbox that checks what a task reads, TypeScript configs, and a core you extend through plugins instead of forking, on a JavaScript monorepo, and you can run pre-alpha software on Bun.
Checkpoint
Section titled “Checkpoint”Your team ticks two needs: “A stable release with a published support policy” and “Undeclared inputs must be caught on our own machines”. Which tools meet both, and which need rules out each of the others? Answer before you open the box, then tick the two needs in the matrix.
Answer
Bazel meets every need you ticked.
vx is ruled out by “A stable release with a published support policy”.
Turborepo is ruled out by “Undeclared inputs must be caught on our own machines”.
Nx is ruled out by “Undeclared inputs must be caught on our own machines”.
The choices that decide it: how a task's inputs are found, what checks the key and maturity and ecosystem.