How many coding agents should you run at once
4 min read
Run one coding agent and you wait. Run twelve and you get a merge conflict festival, a surprising bill, and a laptop that sounds like it is leaving. The useful answer is not a number — it is that you need two separate limits, and most setups only have one.
DevPilot's ConcurrencyManager carries both:
constructor(config: { maxConcurrentSubagents: number; maxTotalActiveTasks: number })
maxTotalActiveTasks is global. maxConcurrentSubagents is per plan. They are
enforced by two different checks and they exist for entirely different reasons.
The global limit is about your machine and your bill
canDispatch(count: number = 1): boolean {
return this.activeTasks.size + count <= this.config.maxTotalActiveTasks;
}
This one is a resource ceiling. Every active agent is a process, a model context, and a rate-limit slot you are spending. Nothing about the work determines the right value — it is determined by your CPU, your RAM, your API tier, and how much you are willing to spend in an hour.
Set it from the constraint that bites first. On a laptop that is usually memory, because each agent holds a working set and the machine starts swapping long before the API complains. On a funded team with a high rate limit it is usually cost: at a few dollars per non-trivial task, a global cap of twenty is a three-figure hour if they all miss.
The mistake here is treating it as an ambition. A high global cap does not make work go faster if the work cannot be parallelised — it just means more agents sitting in contention.
The per-plan limit is about the work
canDispatchToWave(wavePlanId: string): boolean {
const activeForPlan = this.getActiveTasksForPlan(wavePlanId);
return activeForPlan.length < this.config.maxConcurrentSubagents;
}
This one has nothing to do with your machine. It is a statement about how many agents can work inside one piece of planned work without getting in each other's way.
Two agents on the same repository, in the same wave, touching adjacent modules will produce edits that individually pass tests and together do not compose. The per-plan limit is what stops a plan from eating itself, and the right value is a property of the plan's shape — how cleanly its tasks partition by file — not of your hardware.
Which is why a single "max parallelism" setting is not enough. One number cannot answer both "how much can this laptop take" and "how much can this migration tolerate", and when you force it to, you get whichever answer is smaller and never learn which constraint you actually hit.
Start lower than you think
A practical starting point, and the reasoning rather than the numbers:
Global: as many as you can afford to have all fail at once. That framing is more useful than a number, because it makes the cost of a bad plan visible up front. If twelve simultaneous failures is an amount of money you would be annoyed about, twelve is too many.
Per plan: start at two or three. Not because more is impossible, but because the failure mode of too-high is invisible in a way the failure mode of too-low is not. Too low and you notice — things are slow. Too high and you get plausible diffs that conflict subtly, and you pay for that later, in review, where it is most expensive.
Raise the per-plan cap only when you have evidence the plan's tasks are actually disjoint. "Evidence" means file ownership was checked, not that it seemed reasonable when you wrote it — which is a property a plan can be validated for rather than hoped for.
The failure nobody warns you about
The interesting bug is not too many agents. It is agents that never started.
Concurrency limits are enforced at dispatch. When a dispatch path is broken — and there are more ways to break one than you would like — the symptom is not an error. It is tasks sitting in a queued state while the system reports success. Every check passed, the count never incremented, and nothing ran.
We shipped exactly that: a dispatch coordinator whose state lived in a module-level singleton, duplicated across separate build entries so that the coordinator doing the counting and the coordinator doing the dispatching were different objects. Wave tasks queued silently. Dispatch reported success, changed no status, and started no agent.
It hid because the one path anybody tested by hand bypassed the coordinator entirely.
The general lesson for anyone running a fleet: "dispatched" and "running" are different states and you should be able to see both. If your tooling reports only that work was handed off, a broken hand-off looks exactly like a working one. Count what is actually alive — an orchestrator that can tell you how many agents are running right now is worth more than any concurrency setting.
What we have not measured
There is no benchmark here yet establishing where throughput actually peaks against per-plan concurrency. The reasoning above is drawn from the shape of the code and from failures we hit, not from a curve. The benchmark suite that would produce that curve exists and is not yet wired into CI, which is the honest state of it.
When it is, this page gets numbers instead of arguments.
