Skip to content
vxvx
GitHubBlueskydev.toRSS
Blog

$12k a year saved with vx vs Nx Cloud

vx's CI is one run spread over its own Linux and Mac runners. On Nx Cloud's Team plan with Nx's agents, the same CI would cost about $12,192 a year. On vx it costs $0.

vx’s CI runs on its own GitHub runners, spread by @vzn/vx-dte. On Nx Cloud’s Team plan with Nx’s agents, the same CI would cost about $12,192 a year. On vx it costs $0.

Scenario Nx Cloud Team, Nx’s agents vx DTE
this repository $1,016 a month, $12,192 a year $0
a 20-person team $1,902 a month, $22,824 a year $0

This repository: 108 CI runs a day, week to 2026-10-23 · Nx’s public prices read 2026-10-23 · Mac work and concurrent connections not counted · the math

The first green main as one distributed run (#4119, 49adc41) took 1 min 59 s, docs deploy included. It was three jobs before: Linux, arm64 and macOS.

What Count
tasks 129
answered by the local cache 96
downloaded from the remote cache 8
ran on workers 22
workers that ran tasks (4 Linux, 2 Mac) 6
workers started, then cancelled as not needed 13

A Mac or arm worker starts only when the graph has a task for it (exec.platforms). Each worker’s warm-up and time online:

Warm-up
small#119.9 s
small#421.1 s
small#620.3 s
small#819.8 s
mac#1737.2 s
mac#1931.9 s
Online
small#147.3 s
small#458.9 s
small#619.8 s
small#852.8 s
mac#1752.5 s
mac#1915.4 s
The table
Worker Warm-up Online
small#1 19.9 s 47.3 s
small#4 21.1 s 58.9 s
small#6 20.3 s 19.8 s
small#8 19.8 s 52.8 s
mac#17 37.2 s 52.5 s
mac#19 31.9 s 15.4 s
  • The orchestrator is your CI job’s vx run. It plans how many workers the misses need, starts them and hands out tasks. It runs none itself.
  • A worker is vx dte worker in a CI job. It runs what it is sent and saves outputs to your cache under the task’s key.
  • The relay passes messages, since CI runners take no inbound connection. It keeps none: each side numbers its messages and sends one again until the other acks it.
  • The cache carries every file. No file goes through the relay.

Adapters start workers: github(), gitlab(), local() (git worktrees) and docker().

@vzn/vx-dte-server depends only on jose and speaks PROTOCOL.md. It has one route: a websocket that joins as it opens. It pushes each message down the receiver’s socket and keeps no copy. It runs one Durable Object per run on Cloudflare’s free plan, or on Bun in Docker. One var, ALLOW, lists who may join as issuer:owner/repo:workflow.

1 orchestrator and 3 idle workers · before: 88-96 frames, 44-48 each way · after: 24 up, 36 down (#4309) · each participant joins with one request; Workers bills 20 websocket messages as one request, and the relay writes no storage row (pricing, read 2026-10-23) · how it works

Each worker runs a Noise handshake with the orchestrator, on WebCrypto, checked against the cacophony vectors (packages/vx-dte/src/noise.ts).

  • With a run key (VX_DTE_SECRET) it is Noise_NNpsk0_25519_AESGCM_SHA256. On GitHub Actions with no key it is Noise_NN_25519_AESGCM_SHA256, bound by OIDC tokens of the same repo.
  • Keys are ephemeral, so a leaked run key opens no past run.
  • Each task gets only the env its config passes, encrypted.
  • Workers borrow the orchestrator’s short-lived cache token. Nothing is stored.

Only misses need workers. A run the cache answers whole starts none.

  • How many workers: the planner (packages/vx-dte/src/planner.ts) starts the fewest that end within 5% of the best. Idle workers leave.
  • Where a task goes: to a worker with room, holding most of its dependencies’ outputs, then the fullest one.
  • Which first: the longest tail of recorded time.
  • What it books: the cores and memory its history used. No history gets one free core.

Each is a layer you swap with dte({ scheduler }) (packages/vx-dte/src/strategy.ts):

Layer Choices (default first)
order critical-path (HEFT’s rank), graphene (long and wide first, Grandl OSDI’16), fifo
place best-fit (fewest cores left), tetris (needs aligned with what is free, SIGCOMM’14), worst-fit, first-fit
book measured (history), margin (a fifth more, as Borg does), whole (a worker each)
scale plan (the planner above), backlog (cores for what is ready or running, as Spark’s dynamic allocation)

packages/vx-dte/tests/sim.ts plays a run on a virtual clock with the plugin’s own code, so minutes play in milliseconds. Main’s run at c5d8353, with each task’s measured time, CPU and memory:

c5d8353 Real Simulated
run end 113.8 s 112.7 s
CPU used 53% 56%
workers 4 4
cancelled workers 0 0

Every strategy on main’s a09b987 run (65 misses, 4-core workers, 20 s warm-up). The best end on unlimited workers is 120.5 s:

Simulated end
default120.5 s
order: graphene120.5 s
order: fifo120.5 s
place: tetris120.5 s
place: worst-fit120.5 s
place: first-fit120.5 s
book: margin120.5 s
book: whole348.9 s
scale: backlog120.5 s
Worker time
default13.1 min
order: graphene12.7 min
order: fifo14.8 min
place: tetris14.6 min
place: worst-fit14.6 min
place: first-fit13.1 min
book: margin16.2 min
book: whole58.9 min
scale: backlog13.1 min
The table
Strategy Simulated end Workers Utilization Worker time
default 120.5 s 10 86% 13.1 min
order: graphene 120.5 s 10 89% 12.7 min
order: fifo 120.5 s 10 76% 14.8 min
place: tetris 120.5 s 10 77% 14.6 min
place: worst-fit 120.5 s 10 77% 14.6 min
place: first-fit 120.5 s 10 86% 13.1 min
book: margin 120.5 s 12 69% 16.2 min
book: whole 348.9 s 11 19% 58.9 min
scale: backlog 120.5 s 12 86% 13.1 min
Terminal window
bun test packages/vx-dte/tests/sim.test.ts

Five ways, on 4 vCPU machines that take 20 s to start:

  • One machine: every task on one job.
  • Static matrix: N YAML jobs, each a fixed share, tuned once.
  • Build stage, then a matrix: one job builds, N jobs split the rest.
  • Naive DTE: N workers up front, one task each, all stay to the end.
  • vx DTE: the default above.

Machine time is billed 4 vCPU minutes, warm-up included; the 1 vCPU orchestrator counts a quarter · CPU used is work over billed cores · every way but vx DTE played by the ideal packer, vx DTE by the full simulator · bun packages/vx-dte/scripts/compare.ts

vx’s main run, a09b987. 65 tasks, 45.0 core-minutes, a flat graph.

Ends
One machine751.0 s
Static matrix, 10 jobs126.0 s
Build stage, then 10 jobs190.0 s
Naive DTE, 10 workers392.8 s
vx DTE, 10 workers120.5 s
Machine time
One machine12.5 min
Static matrix, 10 jobs18.3 min
Build stage, then 10 jobs18.6 min
Naive DTE, 10 workers67.1 min
vx DTE, 10 workers16.9 min
The table
Way Ends Machine time CPU used
One machine 751.0 s 12.5 min 90%
Static matrix, 10 jobs 126.0 s 18.3 min 62%
Build stage, then 10 jobs 190.0 s 18.6 min 60%
Naive DTE, 10 workers 392.8 s 67.1 min 17%
vx DTE, 10 workers 120.5 s 16.9 min 67%

A vx PR. 16 tasks, 9.2 core-minutes. A bar is a task, as tall as its cores. Orange is warm-up, grey is billed and idle.

A vx PR, as it runs
One machine189.9 s · 3.2 min
Static matrix, 10 jobs90.1 s · 13.2 min
Build stage, then 10 jobs110.1 s · 13.6 min
Naive DTE, 10 workers120.2 s · 20.5 min
vx DTE, 3 workers90.1 s · 4.4 min
Ends
One machine189.9 s
Static matrix, 10 jobs90.1 s
Build stage, then 10 jobs110.1 s
Naive DTE, 10 workers120.2 s
vx DTE, 3 workers90.1 s
Machine time
One machine3.2 min
Static matrix, 10 jobs13.2 min
Build stage, then 10 jobs13.6 min
Naive DTE, 10 workers20.5 min
vx DTE, 3 workers4.4 min
The table
Way Ends Machine time CPU used
One machine 189.9 s 3.2 min 73%
Static matrix, 10 jobs 90.1 s 13.2 min 17%
Build stage, then 10 jobs 110.1 s 13.6 min 17%
Naive DTE, 10 workers 120.2 s 20.5 min 11%
vx DTE, 3 workers 90.1 s 4.4 min 53%

An app monorepo after a base library change. 24 chained libraries, 4 apps, 84 tasks, 71.0 core-minutes.

The app monorepo after a base library change, as it runs
One machine1205.0 s · 20.1 min
Static matrix, 6 jobs810.0 s · 73.1 min
Build stage, then 6 jobs1060.0 s · 27.3 min
Naive DTE, 6 workers765.0 s · 79.7 min
vx DTE, 6 workers720.0 s · 34.7 min
Ends
One machine1205.0 s
Static matrix, 6 jobs810.0 s
Build stage, then 6 jobs1060.0 s
Naive DTE, 6 workers765.0 s
vx DTE, 6 workers720.0 s
Machine time
One machine20.1 min
Static matrix, 6 jobs73.1 min
Build stage, then 6 jobs27.3 min
Naive DTE, 6 workers79.7 min
vx DTE, 6 workers34.7 min
The table
Way Ends Machine time CPU used
One machine 1205.0 s 20.1 min 88%
Static matrix, 6 jobs 810.0 s 73.1 min 24%
Build stage, then 6 jobs 1060.0 s 27.3 min 65%
Naive DTE, 6 workers 765.0 s 79.7 min 22%
vx DTE, 6 workers 720.0 s 34.7 min 51%

The same monorepo, one app changed. 3 tasks, 9.3 core-minutes.

Ends
One machine260.0 s
Static matrix, 6 jobs260.0 s
Build stage, then 6 jobs280.0 s
Naive DTE, 6 workers260.0 s
vx DTE, 1 worker260.0 s
Machine time
One machine4.3 min
Static matrix, 6 jobs10.0 min
Build stage, then 6 jobs7.3 min
Naive DTE, 6 workers27.1 min
vx DTE, 1 worker5.4 min
The table
Way Ends Machine time CPU used
One machine 260.0 s 4.3 min 53%
Static matrix, 6 jobs 260.0 s 10.0 min 23%
Build stage, then 6 jobs 280.0 s 7.3 min 32%
Naive DTE, 6 workers 260.0 s 27.1 min 9%
vx DTE, 1 worker 260.0 s 5.4 min 43%
  • One machine bills least and ends last when there is work to spread.
  • A static matrix fits the graph it was tuned for. On a PR all 10 jobs still start: 13.2 machine-minutes against 4.4.
  • A build stage makes every test wait for the slowest build.
  • Naive DTE keeps every worker to the end: 17% CPU on main.
  • vx DTE ends first or level in all four. One machine bills less when one task sets the end.

Nx Cloud is the closest product. Since 2026-10-22 its dynamic task packing packs tasks by measured CPU and memory, as vx DTE does. The difference is where it runs and what it costs. Running Nx Cloud yourself is its Enterprise plan, at a custom price.

Item Nx Cloud vx DTE
Hobby plan free, 50,000 credits a month, 5 contributors, 10 concurrent CI connections; features turn off when credits run out —
Team plan $29 a month, $29 of usage included —
Extra credits $5.50 per 10,000 —
Each CI pipeline execution 500 credits nothing
Contributors beyond 5 $19 each nothing
Concurrent CI connections beyond 10 $2.25 each nothing
Its agents, Linux x64 5 credits/min per vCPU (4 vCPU: 20) your CI’s own runners
Its agents, Linux arm64 26 credits/min for 4 vCPU your CI’s own runners
Its agents, macOS no resource class listed your CI’s own runners
Self-hosted Enterprise plan, custom price every plan there is: MIT, all of it

Read 2026-10-23 from nx.dev/pricing, Credit Pricing, CI with Nx Cloud · 30-day months, Team plan, a 4 vCPU agent per worker, reports off, Mac work, concurrent connections and taxes not counted · the $29 fee buys $29 of usage

This repository: 760 runs in a week (GitHub’s API, cancelled not counted), 3,257 a month. From the 54 runs after #4150, a PR run kept Linux workers online 4.2 minutes on average, a main run 2.1. That is 567 credits a run with the 500 per execution: 1,846,529 a month, $1,016. The Hobby plan’s 50,000 credits last 88 runs (per-run numbers).

A 20-person team, a made-up case: 200 executions a working day, 21 days, 10 agent minutes each. 2,940,000 credits is $1,617, plus 15 seats at $19. $1,902 a month.

vx DTE has no plan, credits or seats. Its compute is the CI minutes your runners already bill.

Not on npm yet: import dte and github from a checkout’s packages/vx-dte/src/index.ts (README), and host a relay (README):

// in .vx/workspace.ts's plugins
dte({
relay: 'https://relay.example.com',
workers: { small: { max: 8, cpu: 4, memory: '16GB' } },
adapter: github(),
})

The run’s job needs id-token: write and actions: write, and a vx-worker.yml whose jobs run bunx @vzn/vx dte worker. The package README has both workflows, GitLab’s pipeline and every option. vx’s own are ci.yml and vx-worker.yml.

More 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.