Skip to content
Thomas Fairhurst
All posts

13 min read

Hill-climbing our CI with Opus 5.5 agents

Our coding agents merge 50 to 100 changes a day on one 8-core server. In a week, we got the median merge from 16 minutes down to 86 seconds.

Updated

At Leadlord (opens in a new tab), the startup I run with my co-founder James, AI coding agents write most of our code and merge 50 to 100 changes a day, all on one 8-core server. In early October, a merge took a median of 16 minutes to land. A week later it took 86 seconds, 11 times faster. Most of that came from putting merges in a line, which honestly I should have done from the start. Then I worked out from first principles how fast our checks could possibly be, and let a Claude Opus 5.5 agent and 168 subagents hill-climb toward that floor overnight. On real past merges the agents never saw, the checks took 29% less time.

CI is becoming a bottleneck

By August, GitHub was seeing 2.9 billion commits a month (opens in a new tab). In October, it reported 7.38 billion for September alone (opens in a new tab), more than five times as many as a year earlier, though the two posts may not count commits the same way.

GitHub's chart of commits per month on GitHub, January 2023 to August 2026: flat near 0.4 billion through 2023, about 0.75 billion at the start of 2025, 1.5 billion at the start of 2026, rising to about 2.9 billion by August 2026. (full size, opens in a new tab)

Commits a month on GitHub, January 2023 to August 2026.

Source: GitHub, The August 17 outage, and the work ahead (opens in a new tab), 20 Aug 2026.

Checking all that code didn’t get cheaper. In September, GitHub Actions ran 3.26 billion times (opens in a new tab), more than 4× as many as a year earlier. Anthropic saw a 25× increase in CI jobs (opens in a new tab) over six months, and at Linear, test suites have almost quadrupled (opens in a new tab) since January.

GitHub's chart of completed GitHub Actions runs a day, January to August 2026: weekdays rise from about 30 million in January to about 100 million in August, peaking at 115.4 million, with dips every weekend. (full size, opens in a new tab)

Completed GitHub Actions runs a day, January to August 2026.

Source: GitHub, The August 17 outage, and the work ahead (opens in a new tab), 20 Aug 2026.

Our repository shows the same. In the 18 days after Claude Opus 5.5 (opens in a new tab) came out on Sep 22, our agents finished about 71 tasks a day, against 47 in the three weeks before, with a median 704 changed lines each instead of 141 (non-blank lines added plus removed, without generated files or lockfiles). Our main branch went from 0.7 to nearly 2 million lines in those 18 days, and about half the new lines were tests. That’s more code for CI to check, on every change.

The code CI checks nearly tripled in 18 days

Non-blank lines on trunk, end of each day

Lines on trunk per dayTrunk had 471,009 lines on Aug 31 and 697,541 on Sep 21, the day before Opus 5.5 launched. By Oct 9 it had 1,984,806. The biggest day, Sep 23, added 201k lines spread across apps and modules, not imported code.00.5M1M1.5M2MSep 1Sep 15Oct 1Opus 5.50.70M1.98MLines on trunk per dayTrunk had 471,009 lines on Aug 31 and 697,541 on Sep 21, the day before Opus 5.5 launched. By Oct 9 it had 1,984,806. The biggest day, Sep 23, added 201k lines spread across apps and modules, not imported code.00.5M1M1.5M2MSep 1Sep 15Oct 1Opus 5.50.70M1.98M
Non-blank lines in source and text files on trunk at the end of each UTC day (TypeScript, JSON, Markdown, CSS and the like), leaving out generated and vendored code, lockfiles and static assets
DayLinesOf which tests
Aug 31471,009137,500
Sep 1469,208138,101
Sep 2482,376141,220
Sep 3483,693142,362
Sep 4494,340149,499
Sep 5504,556173,270
Sep 6501,237167,509
Sep 7529,997180,685
Sep 8544,332197,184
Sep 9575,322205,504
Sep 10597,530217,343
Sep 11618,092224,064
Sep 12622,067225,188
Sep 13629,052227,090
Sep 14623,441225,776
Sep 15620,822224,618
Sep 16619,702224,461
Sep 17603,879217,535
Sep 18645,452229,934
Sep 19649,647231,320
Sep 20660,541236,142
Sep 21697,541257,532
Sep 22719,853268,948
Sep 23920,930382,829
Sep 241,030,037443,208
Sep 251,061,658459,711
Sep 261,082,909470,586
Sep 271,209,381547,229
Sep 281,283,917591,213
Sep 291,283,917591,213
Sep 301,346,405629,625
Oct 11,393,257657,942
Oct 21,496,329720,046
Oct 31,635,123797,050
Oct 41,677,531817,215
Oct 51,717,147834,079
Oct 61,782,064851,744
Oct 71,862,466888,877
Oct 81,909,173913,505
Oct 91,984,806963,959
Source: Leadlord’s git history.

In late September, Lindy’s CEO Flo Crivello and OpenClaw’s creator Peter Steinberger put it more bluntly:

Same. @useblacksmith (opens in a new tab) has been an amazing sponsor but we need to distribute the load. My plan is to let codex decide which tests actually need to run and drastically nix CI and run tests hourly.

Flo CrivelloLindy@Altimor

CI has become the top bottleneck of every engineering team I talk to (including Lindy). Our CI spend has become stratospheric.

Quoted post by Flo Crivello, on X (opens in a new tab)

Running fewer tests is the obvious fix, but it’s harder than it sounds. When someone suggested a smaller, faster model to pick which tests run, Steinberger replied (opens in a new tab): “Tried; not smart enough.” If every change runs every check, checking grows faster than the work it protects.

Our setup: one server for everything

We run on one rented server in Montreal: an AMD Ryzen 7 9700X with 8 cores and 16 threads, 64 GiB of RAM and two mirrored NVMe drives, for about $98 a month. Production, staging, the development environment, T3 Code (opens in a new tab) (the app our agents run in), one git worktree per agent task and a CI virtual machine all share those 16 threads. We haven’t launched yet, so we accept that for now.

Everything competes for the same 16 threads

What shares our one serverOne server, an AMD Ryzen 7 9700X with 8 cores and 16 threads, 64 GiB of RAM and two mirrored NVMe drives, runs six things: production; staging; the dev environment, one checkout of trunk with live dev servers; T3 Code with every Claude Code and Codex agent session; one git worktree per agent task, where merge gates run; and a CI virtual machine with 16 virtual CPUs and up to 24 GiB of RAM. All six draw on the same 16 threads and 64 GiB: the CI VM's 16 virtual CPUs are the host's 16 threads, not extra ones.ProductionStagingDev environmentT3 Code (agents)Agent worktreesCI VM16 vCPUs16 THREADS8 cores, 2 threads each64 GIB MEMORYCI VM: up to 24 GiBeverything else: 40 GiBWhat shares our one serverOne server, an AMD Ryzen 7 9700X with 8 cores and 16 threads, 64 GiB of RAM and two mirrored NVMe drives, runs six things: production; staging; the dev environment, one checkout of trunk with live dev servers; T3 Code with every Claude Code and Codex agent session; one git worktree per agent task, where merge gates run; and a CI virtual machine with 16 virtual CPUs and up to 24 GiB of RAM. All six draw on the same 16 threads and 64 GiB: the CI VM's 16 virtual CPUs are the host's 16 threads, not extra ones.ProductionStagingDev environmentT3 Code (agents)Agent worktreesCI VM16 vCPUs16 THREADS8 cores, 2 threads each64 GIB MEMORYCI VM: up to 24 GiBeverything else: 40 GiB
The CI VM's 16 virtual CPUs are these same threads, not extra ones. Source: our architecture docs; lscpu and free, Oct 9.

To land its work, an agent merges: a merge gate runs the checks CI would run on everything the change can affect, then squashes the branch into trunk, our main branch. The development environment serves trunk with hot reload, so I can try on my phone what the agents merged an hour ago. A bad merge would break it for everyone, so the gate also builds the apps and boots the dev servers.

Where you can try a change

A classic dev flow compared with oursClassic: a laptop with your own dev server, then a pull request, then cloud CI on a fresh machine per run, then a shared staging environment, which is where QA happens. Ours: an agent worktree on the server, then the merge gate running CI's checks on the same server, then the always-up development environment on trunk with hot reload, which is where QA happens. A merge never deploys. Release is a separate, later step: trunk is pushed to GitHub, CI runs every check again in its own virtual machine on the same server, then staging, then production.CLASSICLaptopPull requestCloud CIStagingQA hereOURSAgent worktreeon the serverMerge gateDev environmentalways upQA hereRelease, laterCI, staging, productionA classic dev flow compared with oursClassic: a laptop with your own dev server, then a pull request, then cloud CI on a fresh machine per run, then a shared staging environment, which is where QA happens. Ours: an agent worktree on the server, then the merge gate running CI's checks on the same server, then the always-up development environment on trunk with hot reload, which is where QA happens. A merge never deploys. Release is a separate, later step: trunk is pushed to GitHub, CI runs every check again in its own virtual machine on the same server, then staging, then production.CLASSICLaptopPull requestCloud CIStagingQA hereOURSAgent worktreeon the serverMerge gateDev environmentalways upQA hereRelease, laterCI, staging, production
In ours, you can try a change right after the merge gate; a merge never deploys. Source: our setup; the classic flow is a common pattern.

CI then runs every check again on each push, as a second, clean check: the four real CI failures since Oct 5 had all passed the gate first.

Three times matter from here on: merge time, from an agent asking to merge to its answer, waiting in line included; gate time, just the checks; and CI time, the check job GitHub Actions runs after a push.

Putting merges in a line

At first, gates didn’t wait for each other: a new one started whenever the memory we forecast it would need fitted beside the others. But gates sharing a machine all finish late, and a gate on a busy server took 1.76 times as long as it did alone.

Why merges wait in line

Four merges, each a 4-minute gate alone

In a line, the first agent hears back at 4 minutes instead of 16. Source: illustration, with gates sharing the server perfectly.

An agent wants its answer fast, to fix a failure and try again. In a line, each gate runs about as fast as it would alone, so, with no cores idle, the average wait goes down. A separate agent session built the line: one heavy gate at a time, the next starting only while the server is healthy. GitHub’s merge queue (opens in a new tab) wasn’t an option, because our merges are local squashes into trunk, not pull requests.

The median merge went from 16 minutes in the three days before the line to 75 s on its first day. Where we logged it, the gate itself had a daily median of 157–235 s, so most of a merge was spent waiting to start. The same change also removed the memory forecasts, so we can’t split the gain between the two.

A line has its own cost: every failure and every slow gate is paid again by everyone behind it.

A test runner twice as fast, and two slow days

We also moved all 3,727 test files from Vitest to Bun’s test runner (opens in a new tab), one process per package, because isolating each file (opens in a new tab) would have cost about 3× on our Effect-heavy packages. CI’s test phase went from 597 s to 296 s, and its CPU time fell 2.7×.

Bun’s test runner: 4.9× faster than Vitest on Bun’s benchmark, 2.0× on our CI

Wall time in seconds; Bun’s benchmark on a log scale

Test runner wall times, seconds. Bun's benchmark: 2,000 TypeScript test files × 8 small tests on a 16-core Apple M4 Max, all cores. Our CI: the full test phase on 3,727 test files.
RunRunnerCommand or runsWall time
Bun's benchmark, one global per workerBunbun test --parallel --no-isolate0.75 s
Bun's benchmark, one global per workerVitestvitest run --no-isolate3.7 s
Bun's benchmark, fresh global per fileBunbun test --parallel6.8 s
Bun's benchmark, fresh global per fileJestjest19.1 s
Bun's benchmark, fresh global per fileVitestvitest run133 s
Our CI, full test phaseVitestmedian of 7 runs597 s
Our CI, full test phaseBun2 runs296 s
Vitest ÷ Bun, Bun's benchmark, one global per worker4.9×
Vitest ÷ Bun, our CI2.0×
Bun’s suite is almost all per-file startup, where Bun is strongest; in our 3,727 real test files, the tests’ own work and the slowest suites cap the gain. Source: Bun docs, “How it compares” (Bun 1.4, Vitest 4.1, Jest 30); our CI runs, Oct 2026.

Two choices made merges slower. First, we swapped our custom per-file test selector, whose store of recorded file reads once wrote 778 GB in 4.5 hours, for Turbo, our build tool, which reruns every package a change affects: a median 13.7% of test time per commit, against 3.4%. Second, Bun’s 5 s default test timeout, down from Vitest’s 30 s, failed about half of gate runs for an afternoon, until one 300 s limit, Bazel’s (opens in a new tab) default for a medium-sized test, replaced it and about 400 per-test limits. The coarser test selection, and packages running their files one at a time at first, more than doubled the median merge for two days, from 75 s to 198 s.

A faster type checker, tsc-rs (opens in a new tab) 0.1.0, checked our 86 projects in 50.4 s, against 112.0 s for TypeScript 7 (opens in a new tab) (both with Effect’s diagnostics). So every gate now type-checks too, at no added wall time.

The theoretical lower bound

Before the climb, I worked out how fast a gate could be. Two results from parallel computing, the work law and the span law (opens in a new tab), give that floor:

  1. Work ÷ threads. With 16 hardware threads, a gate does at most 16 CPU-seconds of work per second.
  2. The longest chain. A test step can’t finish before its slowest test file, however many threads there are.

A gate can’t be faster than the larger of the two. A broad gate (one for a change that touches most of the repository) ran about 1,000 CPU-seconds of tests, about 64 s on 16 threads. But its longest chain was a single backup test file that ran for 129 s.

One test file sets the floor

Server threads busy, average of 37 gates, by second

Share of the server's 16 threads busy during the gate's tests, average of 37 gates (Oct 6–7, 6,297 s sampled; host-wide, so it includes other work on the server; 45% of sampled seconds were under half busy). Floors are for one broad gate on Oct 7, about 1,000 CPU-seconds of tests; real cores count 16 threads as 1.2× the work of 8 cores.
Seconds since the tests startedBusy
0–20 s96%
20–40 s87%
40–60 s79%
60–80 s73%
80–100 s59%
100–120 s43%
120–140 s33%
140–200 s24%–29%
Floor: work ÷ 16 threads64 s
Floor: work ÷ real cores106 s
Floor: longest chain (one test file)129 s
In the tail, about 11 of the 16 threads sat idle. Source: CPU samples of 37 gates, Oct 6–⁠7; floors from Oct 7.

Work ÷ threads is also too optimistic. The 9700X’s 8 physical cores each run two threads, and sixteen busy threads did only about 1.2× the work of eight. Other work on the server slows a gate further.

That leaves two ways in: less work, which lowers both floors, and better packing (start the long steps first, remove late starts, fill idle threads). Packing came with a rule: added CPU, divided by 16, must be smaller than the wall time it saves, because once the gate is busy throughout, added CPU becomes wall time.

The overnight climb

Our agents followed Lance Martin’s Automating eval design and hillclimbing with Claude (opens in a new tab): build an eval (a fixed set of cases that scores every change) whose noise is smaller than the smallest win you’d act on. Hide some cases from the agent proposing changes. Make one change per round, and keep it only if both the tuning and held-out cases improve.

I set the goal and a few rules. The agents designed the eval, measured, made the changes, and merged and published them through the night without asking. Because production shares the server, they needed more guardrails than the method describes:

  • Real past merges. 24 of them, three of each of 8 kinds of change, replayed on today’s code: 16 for tuning and 8 held out. Each round replayed 8 tuning merges, one of each kind, and the score is the expected gate time per merge, each kind weighted by how often it merged in our last 1,000 commits.
  • Noise first. Timing noise was 41% with busy runs included, so every trial waits for a quiet server. The same code against itself then gave 1.7%.
  • Known-bad patches. 17 changes, a flaky test among them, must keep failing the gate: our anti-cheat. If one passes, the change scores zero.
  • “Never weaken a check to make it faster.” No skipping or quarantining tests, no narrowing a step’s inputs, and no change to the eval itself.
  • A fresh reviewer for every change, and one orchestrator session for every merge and publish, with at most 20 subagents at a time.

How the climb ran

How the hill climb ranRounds 1 to 3 ran as a loop of four steps. One: propose a change, in its own worktree. Two: measure it by replaying 8 tuning merges, with run-to-run noise of 1.7%. Three: guard it with a fresh reviewer and known-bad patches that must still fail. Four: keep or revert it on the tuning results; round 1 also ran the held-out merges. Rounds 4 to 7: up to 19 authors worked in parallel; each timed their own change, a fresh reviewer checked it, and it merged without a controlled measurement of its own. 45 merges landed on trunk between round 3 and the final check, climb and other work. At the end, one final check replayed 8 tuning and 8 held-out merges: on the held-out merges, expected gate time per merge went from 84.9 s to 60.6 s, 29% less. The rule every agent worked under: never weaken a check to make it faster.ROUNDS 1–3: MEASUREDnext round1Proposeone change2Measure8 tuning merges3Guardknown-bad patches4Keep or reverton tuning resultsROUNDS 4–7: IN PARALLELOwn timingby the authorFresh reviewMergedFinal check8 tuning + 8 held-outheld-out: −29%How the hill climb ranRounds 1 to 3 ran as a loop of four steps. One: propose a change, in its own worktree. Two: measure it by replaying 8 tuning merges, with run-to-run noise of 1.7%. Three: guard it with a fresh reviewer and known-bad patches that must still fail. Four: keep or revert it on the tuning results; round 1 also ran the held-out merges. Rounds 4 to 7: up to 19 authors worked in parallel; each timed their own change, a fresh reviewer checked it, and it merged without a controlled measurement of its own. 45 merges landed on trunk between round 3 and the final check, climb and other work. At the end, one final check replayed 8 tuning and 8 held-out merges: on the held-out merges, expected gate time per merge went from 84.9 s to 60.6 s, 29% less. The rule every agent worked under: never weaken a check to make it faster.ROUNDS 1–3: MEASUREDnext round1Proposeone change2Measure8 tuning merges3Guardknown-bad patches4Keep or reverton tuning resultsROUNDS 4–7: IN PARALLELOwn timing, fresh review, mergedFinal check8 tuning + 8 held-outheld-out: −29%
From round 4, changes merged on their authors’ timings and a fresh review; the final check measured the whole night together: −29% on held-out merges. Source: the night's logs and final eval; Lance Martin's method.

Round 1 ran the browser tests in parallel workers. The tuning merges got 7% faster and the held-out ones didn’t, so it was reverted as overfitting: the method doing its job. To move faster, changes from round 3 on merged without a held-out check of their own: round 3’s on a tuning-set win alone, later ones on their authors’ timings and a fresh review. Only the final eval measured them, together. In about 29 hours, one orchestrator and 168 subagents merged 77 changes.

What the agents found

The week’s biggest single wins, each timed on its own:

Four changes, 5× to 11× faster each

Seconds, before → after; each card on its own scale

Single changes, each timed on its own scope
ChangeWhenMeasured onBeforeAfterSpeed-up
A leaked 30 s sleepOvernight climb, Oct 8One test suite, inside the gate33 s3 s11×
React’s 300 ms waitsOvernight climb, Oct 8Product UI tests, quiet machine~50 s~5 s10×
Pruned image buildOvernight climb, Oct 8Rebuild a service image for a deploy, one-file change142 s18 s7.9×
Backup tests built onceBefore the climb, Oct 5–7The suite holding the 129 s test file147 s27 s5.4×
Each timed on its own scope; a suite shortens a merge only when it’s the slowest step. Source: the agents' timed runs, Oct 7–⁠8; CI history, Oct 5–⁠7.

Two of them were sleeping tests. React 19 waits 300 ms before showing content whose data arrives outside its act() test wrapper, and one suite did that 146 times. Elsewhere, a race left a 30-second sleep holding a suite’s output open. Separately, each test step’s cache key now covers exactly the files its tests read, which fixed 15 steps that could have reused a stale cached pass.

Gate tests run at low CPU priority to keep production responsive, so under load, tests with hand-picked time limits ran past them. Now a test that passes only on rerun still fails the gate, and flakes get fixed at the cause instead of retried. Of 81 failed tries on Oct 9–10, 5 were flakes, and most of the rest were a check catching a real problem. No CI check on trunk failed (0 of 33).

Results: from 16 minutes to 86 seconds

We compare three full windows (UTC): Oct 2–4, before the line; Oct 5–7, with the line but before the climb; and Oct 9–10, the two full days after it. Oct 8, the night of the climb, is in none of them.

Every landed merge Oct 2–4 Oct 5–7 Oct 9–10
Merges 240 175 117
Merge time, median 965 s 172 s 86 s
Merge time, p90 2,422 s 388 s 299 s
Under 2 minutes 9% 27% 59%
Gate time, median, where logged 218 s (48 merges) 155 s (98) 68 s (111)
CI check time per landed merge 234 s 82 s 53 s

Counting every merge, the median got 11 times faster end to end, and the 90th percentile went from 40 minutes to under 5. CI’s check job went from a median of 407 s to 193 s. With more merges in each push, CI now spends 53 s of check time per landed merge instead of 234 s.

A merge also slows down when unrelated work shares the server, which says nothing about the gate. So for the climb’s before and after, we also compare merges on a quiet server: those during which work other than merges and CI used at most 3.2 of the 16 threads on average, the 20% our own tooling counts as contention. That keeps 70 of 175 merges from Oct 5–7 and 40 of 117 from Oct 9–10 (Oct 2–4’s server was rarely quiet enough). Their median went from 159 s to 63 s, and 73% now land in under 2 minutes, against 24%. To be fair, much of that gain is winning back the migration’s two slow days.

On a quiet server, merges went from 2.7 minutes to about a minute

One dot per merge on a quiet server

Landed merge time on a quiet server (non-merge work at most 3.2 of 16 threads over the merge; other merges and the CI they start don't count), seconds, two complete windows (UTC)
WindowQuiet merges keptAll landedExcludedMedianp90SlowestUnder 2 minutesOver 5 minutesSource
One gate at a time, Oct 5–77017510515925592217 of 70 (24.3%)4 of 70 (5.7%)rebuilt from one founder's logs
After the climb, Oct 9–1040117776318064029 of 40 (72.5%)3 of 40 (7.5%)live merge ledger, both founders
Seven in ten merges now land in under 2 minutes, against one in four before the climb. Source: our merge ledger, Prometheus and system log; quiet = work other than merges and CI ≤ 3.2 of 16 threads.

The controlled eval leaves out the server’s load and the migration. It replays each merge on trunk as it stood just before and after the climb, on a quiet server, so it measures everything merged that night together, other tasks included. On the 8 held-out merges, the expected gate time per merge went from 84.9 s to 60.6 s: 29% less.

On 8 real merges the agents never saw, gate time fell 29%

Gate time per replayed merge, seconds

Gate wall time per replayed merge, seconds, before and after the climb
Kind of changeSetBeforeAfterChangeCPU-s beforeCPU-s after
Docs onlyHeld out1913−30%9271
One packageHeld out2514−45%20882
Several packagesHeld out2919−34%265137
Shared libraryHeld out147111−25%1,5831,229
An appHeld out6034−43%363200
Backend (Convex)Held out150112−25%1,6001,244
ToolchainHeld out155121−22%1,7431,408
LockfileHeld out156120−23%1,7421,410
Docs onlyTuning set1913−30%9272
One packageTuning set2816−41%248121
Several packagesTuning set9467−29%911618
Shared libraryTuning set7658−24%754443
An appTuning set6334−46%361201
Backend (Convex)Tuning set10571−32%1,173744
ToolchainTuning set10274−28%1,120811
LockfileTuning set154120−22%1,7331,407
Held out, weighted by how often each kind mergesHeld out84.9 s60.6 s−29%
Tuning set, weighted the same wayTuning set71.4 s48.8 s−32%
Held out, typical merge, unweighted (geometric mean)Held out−31%
Gate CPU time, all 16, weighted the same way (unweighted)Both−41% (−35%)
All 16 merges got faster, by 22–⁠46%; the tuning merges fell by about as much (32%), so the gains carried over. Source: our controlled merge-gate eval, final run, Oct 9.

Test coverage held up: the share of product files run by at least one test rose from 94.0% to 96.4%, and across all files it went from 91.5% to 91.0%.

About the data: we rebuilt Oct 2–7 from one founder’s logs, and Oct 2–4 include a few hung merges. Oct 9–10 come from the live merge log, with both founders’ merges, and from partway through Oct 9 merges were also gated together. CI isn’t in the eval, because its replay runner never finished a full run, so the CI numbers are production data.

Next: fast merges in busy hours

The gate is short now, but merges still slow down when production, agents and CI all run at once. Since Oct 9, we gate waiting merges together, like bors (opens in a new tab) and Mergify’s batches (opens in a new tab) do, and if a batch fails we refuse only the change at fault. Through Oct 10, 120 of 128 batch gates held a single merge, so the line is mostly empty now. What’s left is shared load and memory, and that’s where we’re looking next.

What one server costs

Before the server, we got by on managed services’ starter plans. Vercel hosted the apps, Convex ran the backend, Trigger.dev the background jobs and Fly.io a Discord bot. CI ran on GitHub’s included minutes until we used them up, then on Blacksmith’s free tier, and the agents ran on my laptop until it throttled so much I couldn’t use it.

On Aug 19, Convex disabled all our projects, production included: we had “reached your configured monthly spending limit threshold of $NaN”, though our invoices up to Aug 8 showed us using under a fifth of the plan’s included usage. Yes, $NaN. With usage pricing, you choose between a spending limit that can take you offline and a bill with no ceiling. I’d already been pricing servers, and the next day I rented one.

It now runs all of it. Each environment has its own copy of Convex’s open-source backend (opens in a new tab), Convex’s own scheduler replaced Trigger.dev, and our CI and merge gates run at no extra charge. So the fair comparison is today’s workload, with and without the server:

Today’s workload Without the server With the server
App hosting Vercel Pro, ~$71 a month on the server
Backend Convex Pro, $50 a month plus usage open-source Convex
Background jobs Trigger.dev, $10 a month plus compute Convex’s scheduler
Discord bot Fly.io, ~$6 a month on the server
CI and merge gates Blacksmith, ~$170–300 a month on the server
Dev environment and agents cloud agent VMs, nothing shared on the server
Total about $310–440 a month, plus usage about $98 a month, flat

The first four rows are our last bills or today’s plan prices. Blacksmith (opens in a new tab), a hosted runner service, is a strong option: about a quarter cheaper per minute than GitHub, with a clean machine for every job, so busy hours wouldn’t slow merges down. Scaled from our measured job times, our CI plus about 85 merge gates a day would cost roughly $170–300 a month there.

Every managed plan also has limits:

  • Convex Pro: 50 GB a month each of data egress and database I/O before overage, and 300 deployments per team.
  • Vercel Pro: functions stop after 300 s by default, builds after 45 minutes, and runtime logs last a day.
  • Trigger.dev Hobby: 50 concurrent runs, 100 schedules and logs for 7 days.
  • GitHub-hosted runners: 6 hours per job and a 10 GB cache per repository.
  • Cloud agent VMs, like Claude Code on the web (opens in a new tab): 4 vCPUs, 16 GB and no background processes once the VM pauses.

The server’s limits are the hardware’s.

Having the agents on the same box matters as much as the price. They share one filesystem, one dev environment, one merge line and one Turbo (opens in a new tab) build cache. So the merge gate restores any step an agent already ran, and an agent can run the real app, read the server’s logs or wait on another agent’s run.

It isn’t free. About 17% of our commits since we moved, on Aug 20, were server work, and we’ve had real incidents, from errors across production after our reverse proxy was recreated to an agent’s render jobs running the server out of memory. Agents wrote most of those commits, which makes running our own servers affordable for two people. One $200 Claude Max plan covers about 24 billion tokens a month by our count, so the climb’s 5.24 billion tokens, $2,100–2,500 at API prices, cost us about $43.

What I’d tell another team

  1. Keep patches the gate must refuse. When agents optimise your CI, score any change that lets one through as zero, instead of trusting agents to be careful.
  2. Put merges in a line. Think of merges as jobs: unless gates leave cores idle for most of their run, one at a time gives a shorter average wait.
  3. Work out the floor first, in physical cores. The larger of work ÷ cores and the longest chain tells you what to attack.
  4. Look for sleeping tests. Some of our biggest wins were tests waiting on timers.
  5. Measure big migrations in production too. Our test-runner switch was right in principle and still more than doubled the median merge for two days.
  6. Price the whole setup, not one service. Merges, the backend, the dev environment and the agents all need machines.

Code will keep getting cheaper to write. Checking it only gets cheaper if you measure where the time goes, and let agents climb only where the eval can catch them cheating.

Sources

Agents and CI

Our setup

Costs and limits (prices and limits checked 11 Oct 2026, all in US dollars; our bills in Canadian dollars converted at the ECB rate of the day they were charged)

Tools

Method

Merge queues