Skip to main content
CI as one automation and a config file: every push and pull request runs on fresh machines created from a snapshot of main’s last green run, and reports a GitHub check. Both files are below.

A push to main: nine 2 vCPU machines, green in under two and a half minutes, saved as the next snapshot. Waits are cut short.

Why run CI on boxd

  • The machine is the cache. Jobs start from a snapshot with the repo, dependencies, build caches and Docker layers in place.
  • No idle runners. Machines exist only while a job runs, metered per second.
  • Parallel by default. Each test shard gets its own machine, with no setup to repeat.
  • Isolated. Job machines hold no credentials, so pull request code can’t reach your organization.
  • A real machine. Docker, systemd and Postgres run normally.

What you build

A push or pull request on GitHub starts a run. boxd creates the job machines, one for the checks and one per test shard, all at once from the snapshot of main's last green run. They post a GitHub check on the commit, and a green run on main is saved as the next snapshot. You need the boxd GitHub App on the repository, and one machine to run the automation on. That machine can sleep: an event wakes it.

1. Snapshot your repo

GitHub triggers the run and shows the result. boxd runs it, with no workflow file and no Actions minutes. Set up a machine the way a job should find it, then save it.
Inside the machine:
Back on your laptop:
You do this once. After that, green runs on main save the next version (step 6).

2. Describe your CI

One entry per repository:
ci.json

3. Start a run on every push and pull request

Subscribe to events through the GitHub App. There is no webhook to host:
ci.run.ts

4. Create a machine per job

Create an isolated machine from the snapshot and check out the commit. The GitHub App token is short-lived and exists only in the fetch command’s environment:
The snapshot already holds main’s checkout, so the fetch brings only the new commits.

5. Run everything at once

One machine for the checks and one per shard, all created together:

A pull request: checks and 8 test shards on nine 2 vCPU machines, green in 145 seconds. Waits are cut short.

Report the result as a check on the commit:

6. Promote a green main

After a green run on main, the machine holds the state the next job should start from. Save it once the result is reported, so nobody waits on the save:
Pull requests never write to the snapshot.
A snapshot can’t be used to create machines while it saves a new version. Keep two names (web-main and web-main-b), save into the idle one, then switch.

7. Turn it on

The config, then the automation started with one command.

Push a commit and follow the run with run logs 2f5c7 -f:

The complete files

Copy both into a folder on one of your machines, edit ci.json, and start with run ci.run.ts.
ci.json
ci.run.ts

How fast is it

The CI of 10 open-source projects, three ways: one job on a GitHub-hosted runner, the same shards on GitHub-hosted runners, and the setup above. Every machine on both sides has 2 vCPU. boxd is 6× faster than the single job and 3× faster than the sharded one (medians). Each run is the project’s lint, format, typecheck and test jobs on a push to main. Times are medians, from the push to the result.

Where the speed-up comes from

  • Sharding helps the big suites on any runner. Django drops from 12m 54s to 3m 06s on GitHub’s own runners. It does nothing for small ones: every GitHub job repeats checkout, toolchain setup and install, so ripgrep gets slower when split.
  • The snapshot is the rest, the “vs sharded” column. A boxd job starts with dependencies, build output and caches already on disk, so a shard is running tests a few seconds after the push.
  • Machine size matters little. On 8 vCPU machines with 8 shards, 6 of the 10 projects finished within 16% of their 2 vCPU time. Prettier, React and Excalidraw needed 2 to 4 times the shards on 2 vCPU to come close.
  • Projects. Each at its latest commit on 8 October 2026, copied into a private repository. Excalidraw is at 53973c3.
  • Scope. The project’s Linux lint, format, typecheck and unit-test jobs, with one toolchain version. Left out: Windows and macOS jobs, end-to-end suites, jobs that need external services, release and docs jobs.
  • GitHub, one job. ubuntu-latest in a private repository (2 vCPU, 7 GB), with the ecosystem’s standard setup action and dependency cache. One warm-up run to fill the cache, then two measured runs.
  • GitHub, sharded. The same runners and caches, as one checks job plus one job per test shard. Two measured runs.
  • boxd. The setup on this page with 2 vCPU / 8 GiB machines: checks on one, each test shard on its own, all created from the snapshot. One warm-up run, then three measured runs.
  • Shards. 8 per project. Prettier uses 16. React and Excalidraw use 32 on boxd and 16 on GitHub, where the plan used for the test runs 20 jobs at once.
  • Commands. The same on all three, apart from the shard flag and on-disk cache flags (eslint --cache, tsc --incremental) on boxd.
  • Push to main. A commit with no code change, the steady state between merges.
  • Timing. Wall time from the event to the result, median of the measured runs.
At list prices: 0.006perrunnerminuteonGitHub,billedperjobandroundeduptotheminute;0.006 per runner minute on GitHub, billed per job and rounded up to the minute; 0.059 per vCPU-hour and $0.018 per GiB-hour on boxd, metered per second. The boxd figure assumes every machine holds all 8 GiB for the whole run, so it is an upper bound.Sharding trades compute for time on either platform. boxd costs less than the sharded GitHub run for 8 of the 10 projects, because no shard pays for setup. React and Excalidraw run 33 machines each, twice the GitHub shard count.
  • Sharding has limits. One slow test module sets Django’s time at any shard count. Svelte gains least: one test file holds most of the suite’s time, and its build step has no cache.
  • Checks run in sequence on the first machine. For Zod they take as long as the tests, so more shards change nothing.
  • Go caches test results. On an unchanged commit, GitHub CLI on boxd mostly replays results cached in the snapshot, so its row measures cache coverage more than test speed.
  • Retries and skips, the same on all three. Excalidraw, Vue and Zod run with --retry=2. Django skips one timing-sensitive test and React one test file (4 tests). React’s single hosted job needs two extra Jest flags to fit in 7 GB.
  • A snapshot carries state. Prettier’s lint leaves a format cache that its own CLI tests race to delete, so the shards remove it first. Pydantic’s plugin tests depend on which files share a shard.
On the hosted runner the same job spends 50 to 80 seconds on setup (cache restore and install) and runs the tests in 348 to 537 seconds.

Good to know

  • The slowest shard sets the time. Split tests by measured duration, or add shards.
  • More machines, not faster cores. A boxd vCPU is about as fast as a hosted one. The time comes from the snapshot and from shards running side by side.
  • Machine size comes from the snapshot. Jobs get the size of the machine you saved. Resize it before saving to change that.
  • Steps are plain commands. Marketplace actions (uses:) don’t apply.
  • Warm state can hide problems. A dependency removed from package.json can still be in the snapshot, so a job passes where a clean clone would fail. Run a cold build regularly, for example nightly, to catch that.