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
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.main save the next version (step 6).
2. Describe your CI
One entry per repository:3. Start a run on every push and pull request
Subscribe to events through the GitHub App. There is no webhook to host: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: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.
6. Promote a green main
After a green run onmain, the machine holds the state the next job should start from. Save it once the result is reported, so nobody waits on the save:
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.
run logs 2f5c7 -f:
The complete files
Copy both into a folder on one of your machines, editci.json, and start with run ci.run.ts.
ci.json
ci.json
ci.run.ts
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).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.
How this was measured
How this was measured
- 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-latestin 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.
What a run costs
What a run costs
What the numbers don't show
What the numbers don't show
- 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.
Where the time goes on boxd (Excalidraw)
Where the time goes on boxd (Excalidraw)
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.jsoncan 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.