We moved a production TypeScript/Go monorepo to Boat runners. The previous provider kept failing to prepare machines and left jobs queued for up to 24 hours, so the move made sense. What I didn’t expect was how much the shape of the CI had to change.
Boat bills and caps per machine start, not per minute. Every GitHub job that gets a runner costs one start. A job skipped by if: costs nothing. A job cancelled after it got a machine already paid.
Under per-minute billing, many small parallel jobs are nearly free. Under per-start billing they are the bill.
What the old CI cost
The layout was the usual one: a change-detection job, then separate jobs for TypeScript quality, live database suites, Playwright, Go, image builds (one machine per image), Python, coverage, an LLM benchmark, a docs gate, and an aggregate “all green” job. Test jobs on main had cancel-in-progress: true, which is the right call when you pay per minute.
The plan allowed 840 starts a day, 210 an hour and 30 a minute. The numbers:
- About 3.9 starts per CI run.
- The heaviest day had 964 starts, over the 840 cap. Its peak minute had 42, over the 30 cap. Both limits broke on the same day.
- One day on main, 31 of 52 push runs ended cancelled, and 55 quality jobs had started before the next push cancelled them. Every one of them paid a start.
What it looks like now
One heavy job per pull request run. It does the work of the old jobs as background tasks running in parallel on one bigger machine, and still publishes a separate check per task.
The result is 1 start per PR run, about 2 on main (quality and deploy). The estimated heaviest day is around 270 starts, 32% of the cap. The heavy job takes about 380 seconds.
Before and after, in numbers
Before Boat, this CI lived in two other places: two dedicated servers of my own with runners that were always on, and then a provider that started a new VM for every job and billed by the minute. I pulled every run of each stage from the GitHub API and measured the same things in all of them.
| Own servers | Per-minute provider | Boat, unchanged | Boat, with the skill | |
|---|---|---|---|---|
| Machines | 2 dedicated servers, 12 vCPU / 31 GB and 8 vCPU / 15 GB, always on | 1 VM per job, from 1 vCPU / 2 GB to 4 vCPU / 16 GB | 1 VM per job, 2 vCPU / 4 GB or 8 vCPU / 16 GB | 1 VM per run, 16 vCPU / 32 GB |
| Starts per PR run, median | none (9 jobs on runners already up) | 9 | 3.5 | 1 |
| Heaviest day, in starts | — | 2,112 | 964 | 111 in its first 22 hours |
| Wait for a machine, p90 | 18 min | 26 min | 14 s | 19 s |
| PR to green, median | 19 min | 10 min | 5.4 min | 7.2 min |
| PR to green, p90 | 62 min | 34 min | 10 min | 12 min |
A start is a job that got a machine, counted per pull request CI run, reruns included. The wait is how long a job sat in the queue before it got one. PR to green is the time from opening the pull request to the CI finishing green, for pull requests whose first run passed. The ones that needed a fix measure how long the fix took, not the CI.
What the table says:
- My own servers had no starts to pay for, but they had a queue. Bursts of branches landed on the same two machines, and at the 90th percentile a job waited 18 minutes for a free runner.
- The per-minute provider halved the median time to green, but not the tail: 26 minutes of waiting at the 90th percentile. And 9 machines per run is fine when you pay by the minute, but the same shape on Boat would have been more than 2,000 starts on a busy day, well over the 840 cap.
- Boat with the old layout already removed the queue, and with the work spread over several machines it was the fastest to green. It was also the 964-start day.
- With the skill it is 1 start per run. The median to green went up about 2 minutes, from 5.4 to 7.2, because the work now shares one machine; the 90th percentile barely moved. Two minutes for less than a third of the starts is the trade I wanted.
This isn’t a clean experiment. They are different weeks with different amounts of work, the code kept changing along the way, and the last column covers less than a day, 42 pull requests. Read it as a direction, not a final number. The windows were October 12 to 23, October 25 to 28, October 29 to November 1, and November 1 to 2, 2026.
The skill
I packaged the migration as a skill, boated-ci, so an agent can do it on another repo. It follows the same order I did:
- Audit first. A script reads the GitHub API and reports starts per day, per peak hour and per peak minute, by job, label and event, with a verdict against your plan. If your peak stays under about half of every cap, it says not to restructure and to only fix the waste, like full reruns and cancelled jobs on main.
- Consolidate. Small jobs become background tasks inside one heavy job, with
startandwaitso install runs once instead of once per job. Deploys, other OS or arch, and jobs holding secrets the PR code must not see stay as their own jobs. - One check per task. A single job’s log reached about 195k lines, and the agents fixing PRs had to read it. Now each task publishes a check named
<job> / <task>with only the trimmed failure, using extractors for bun test, go test, Playwright, tsc, eslint, oxlint, golangci-lint, pgTAP and a generic tail. Task logs are masked before upload. - Skip on rerun.
gh run rerun --failedskips tasks that already passed on the same merge commit. It only trusts checks published by the workflow’s own token. - Measure before resizing. A sampler records whole-machine CPU, steal and memory every 2 seconds and slices it per task. Some VMs had 15 to 29% CPU steal, which made them 50 to 70% slower. Without measuring that, you compare noise.
The scripts are Bun and TypeScript with no npm dependencies, and they have tests. There is also an end-to-end workflow template to start from.
Everything is plain GitHub Actions; Boat is just the case that made me write it. The pricing and plan limits in the references were collected around November 1, 2026, so check your provider’s current numbers.
Install it
npx skills add OmarAlex24/skills --skill boated-ci
It works with any agent that reads skills: Claude Code, Codex, Cursor, opencode. Then ask it about your CI, for example: “We hit the starts-per-day cap yesterday. Audit .github/workflows and plan how to consolidate our CI.”
The skill page shows the full instructions, and the source is on GitHub.
The box-reuse router ideas in the references (reuse boxes after a light reset, a shared start ledger with pacing, local machines first) come from pnkfluffy/boat-router.