DEV Community

Cover image for $12k a year saved with vx vs Nx Cloud
VX
VX

Posted on Edited on Originally published at vznjs.github.io

$12k a year saved with vx vs Nx Cloud

Alpha

@vzn/vx-dte and @vzn/vx-dte-server are alpha. They are open source,
MIT, in the vx
repository
, and
vx's own CI runs on them every push.

Names and options may still change.

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

One run, two minutes, six workers

The first green main as one DTE 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:

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

How the pieces talk

diagram

  • 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().

The relay stores nothing

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

diagram

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

The relay reads nothing it carries

diagram

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.

How a run schedules

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)

The simulator matches real runs

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:

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
bun test packages/vx-dte/tests/sim.test.ts
Enter fullscreen mode Exit fullscreen mode

Other ways to split a run

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.

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 minStatic matrix, 10 jobs90.1 s · 13.2 minBuild stage, then 10 jobs110.1 s · 13.6 minNaive DTE, 10 workers120.2 s · 20.5 minvx DTE, 3 workers90.1 s · 4.4 min
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 minStatic matrix, 6 jobs810.0 s · 73.1 minBuild stage, then 6 jobs1060.0 s · 27.3 minNaive DTE, 6 workers765.0 s · 79.7 minvx DTE, 6 workers720.0 s · 34.7 min
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.

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.

What it costs, next to Nx Cloud

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

diagram

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.

Try it

bun add -d @vzn/vx-dte, and host a relay
(README):

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

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.


Originally published on the vx blog. vx is open-source (MIT) task orchestration with a build cache: vznjs.github.io/vx · GitHub.

Written with AI assistance.

Top comments (0)