← Blog

When to add another bay runner

Gudjon Gudjonsson · Founder and CEO of OZ · 28 October 2026

bay is for teams that run many coding agents in one codebase, often on the same files. Each agent submits once and moves on. A runner then checks each change on the latest main, and merges what passed. So when changes wait, the first idea is another runner. Sometimes that is right. Often the wait comes from somewhere else. This post says when another runner helps, and what one team learned when its queue grew.

What one head can do

Every merge needs a check run on the exact latest main. When one change merges, main moves, and a result for the old main no longer counts. Without batches, the head sets the pace: build, checks and merge, one change at a time. That is at most 1,440 ÷ the head's minutes merges a day. With four-minute heads, that is at most 15 an hour, however many runners you start.

What batches change

With batch, the head's runner checks the head and the changes behind it as one tree. On a pass, they all merge in one push, so one run can merge up to max changes. A batch is sized to hold about one expected failure: at one failure in four, four changes. A change that conflicts is passed over and keeps its place for its own turn.

toml
[runner]
batch = { min = 2, max = 8, max_failure_rate = 0.3 }

What another runner adds

  1. A backup: a second runner takes over a stalled claim after one lease, 60 seconds by default.
  2. An environment: a check environment with no runner leaves its changes waiting with CHECK_UNAVAILABLE.
  3. The next batches: while the head's batch is checked, other runners check the batches behind it.
  4. CPU: when one machine is full, first try parallel_checks, then give the next checks their own machine.

What one team saw

One team ran about 40 agents' changes an hour into one main, with checks of about four minutes. bay merged 12 to 15 an hour, one at a time, so the wait grew all evening. From the outside it looked stuck. The runner's own log showed it working, one change after another. A second runner made the queue safe from one machine's failure. Alone, it did not change that arithmetic. Batches did, once they were on.

A wait that was not about runners

The team's longest wait came from merges outside bay, not from too few runners. Main moved every few minutes through another tool, so each check went stale. One change passed its checks five times and never merged. When main held still, seven changes merged in a row, each on its first check run. So send every merge through bay first. Then count runners.

Look before you add one

bay status --all lists each runner with its environments, last pass, held change and queue depth. A change's waiting_for says why it waits. runner means no runner has claimed it: add one, or check the one you have. worker means no runner serves its check environment. queue means it waits its turn behind other changes. Batches move it sooner than another runner. A merged batch's detail.batch.stopped says why it ended; the runner's journal says why one never formed.

sh
bay status --all     # runners: machine, environments, last_pass, holding, queue_depth, stale

Get started

Run at least two runners, on machines that serve the environments your checks name. Turn on batch, then watch waiting_for before you add a third.

sh
bay runner install                                           # keep a runner going on this machine
git config --global --add bay.checkEnvironment linux-x86_64  # the environments it serves

Gudjon Gudjonsson · Founder and CEO of OZ