Adding capacity never removes a constraint, it relocates it. I’ve been running fleets of coding agents since I managed a swarm of 20 of them for a week last August, and over that year the bottleneck has moved three times: from writing the code, to reviewing it, to the physical machine on my desk. I can already see the next one forming, and it’s a product problem, not an engineering one.
Somewhere in there my job changed shape too. I spend less time clearing any one queue and more time watching where the next one forms. If agents made your team faster, the useful question is where the work piles up now.
%%{init: {"look": "handDrawn"}}%%
flowchart LR
A["Writing the code"] --> B["Reviewing it"] --> C["The machine on my desk"] --> D["The cloud"] --> E["Knowing what to build"]
The bottleneck’s path: each stage solved pushes the queue one step downstream. I’m at the machine today; the last two are where it’s headed.
Writing code went first
Nowadays I write tickets the way we always did: a bunch of them, self-contained, with clear acceptance criteria. I suppose it’s not 100% the same, since now it’s mainly me blabbing for 10 minutes and having Claude or another AI transform my word salad into tickets, but you get the point.
Then I point a fresh agent at the queue with a /backlog-clearing skill, each ticket built in its own isolated worktree. Sometimes the agent works through them one at a time; more often I encourage it to fan the tickets out to subagents, as many in parallel as the work allows, keeping its own thread as the orchestrator so its context stays clean.
%%{init: {"look": "handDrawn"}}%%
flowchart LR
Q["Ticket backlog"] --> O["Orchestrator agent<br/>(thread stays clean)"]
O --> S1["Subagent · worktree"]
O --> S2["Subagent · worktree"]
O --> S3["Subagent · worktree"]
S1 --> P1["PR"]
S2 --> P2["PR"]
S3 --> P3["PR"]
One orchestrator fans a backlog out to subagents, each building in its own worktree; a typical run lands 5 to 15 PRs.
The hands-off end of the spectrum was an overnight run: two instructions in (“clear the backlog” and “merge them all”), and by morning it had merged 10 PRs in dependency order. Days end by queuing the backlog. Mornings start by interrogating what merged. Meetings start with me checking if I have a train running so that I have something to look at once the meeting is over.
The tracker (Linear, Jira, etc) holding this together was built for it from the start.
A tracker is just a state machine humans already run: your partner claimed the ticket, so you don’t touch it; there’s a comment, so you know where things left off. The agents keep the same discipline (claim the ticket, keep the status true, comment as you go), which is why parallel sessions don’t duplicate work. Two sessions once collided on the same epic anyway, detected it themselves, and coordinated merge windows. The overnight run’s last status line: “In Progress: zero, no ticket is lying about its state.”
%%{init: {"look": "handDrawn"}}%%
stateDiagram-v2
[*] --> Open
Open --> Claimed: agent claims it
Claimed --> InProgress: work and comments
InProgress --> PR: opens a PR
PR --> Merged: merged in order
Merged --> [*]
Writing the code is the part I never wait on anymore.
Then review became the queue
When you have 10+ PRs overnight, it’s impossible to read every diff deeply. Every PR in my setup ships with proof of each acceptance criterion attached; the full contract is its own post. Review became “inspect the proof” instead of “reproduce the work,” and so far my highest throughput with that method is roughly 40 PRs in a work day.
What I look at moved up a level too. Recently I was adding functionality to a codebase I hadn’t touched before, and my real questions were architectural: what’s the testing policy and are these changes following it, did we introduce new dependencies, do the systems intersect the way they should.
%%{init: {"look": "handDrawn"}}%%
flowchart LR
subgraph Before["Old review"]
direction TB
B1["Read the diff"] --> B2["Reproduce the work"] --> B3["Approve"]
end
subgraph After["Review at fleet speed"]
direction TB
A1["Inspect the attached proof"] --> A2["Ask architectural questions:<br/>testing policy, new deps,<br/>system intersections"] --> A3["Approve"]
end
Before -. moved up the stack .-> After
Review stopped meaning reproduce the work and started meaning inspect the proof, then check the architecture.
Why the code itself stopped being worth senior review time is an argument for its own post; here it's enough that review moved up the stack, and the queue moved on.
Then the machine tapped out
I work several projects at once, and each one usually has three to six agents going, so 12 to 16 top-level agents running at a time is normal. These aren't simple loops, either: they spin up browsers to test their own work, they run Docker, and their subagents spawn subagents of their own, so at any moment it can be dozens of agents doing various things.
What caps the parallelism now is the machine itself: the computer sometimes just dies on me.
%%{init: {"look": "handDrawn"}}%%
flowchart TB
P["3 to 6 projects"] --> A["12 to 16 top-level agents"]
A --> B["Browsers for self-testing"]
A --> D["Docker sandboxes"]
A --> Sub["Subagents spawning subagents"]
B --> W["RAM wall<br/>(the machine dies)"]
D --> W
Sub --> W
Every top-level agent spawns more processes than it looks like, and they all draw down the same finite pool.
The machine is a 32GB Mac mini, and RAM is what maxes out first. (It’s not even an old machine; I’m a little ashamed to admit I bought it during the OpenClaw craze, wanting to run my own instance.) On the 40-PR day it lagged so badly that I had an agent session diagnose what was eating the machine. The diagnosis came back with real numbers:
- 27GB of swap in use
- 3 orphaned worker processes holding roughly 2.5GB
- 7 stale sandbox containers, idle for anywhere from 30 hours to 3 days
- one background service pinned at a constant 28% CPU
The agent ranked the fixes by relief per disruption and documented a one-line restart for everything it killed. The constraint at the end of this arc was memory, not model intelligence, token budgets, or my review speed. There’s a 128GB M5 Mac on the way and I’m excited to push it to its limits, but a bigger machine only moves the wall back; it doesn’t remove it.
The machine’s queue drains to the cloud
I was using Devin when it first came out, back at the end of 2024, and I’ve been running coding agents in the cloud since we built our own at work. What’s changed is how little it takes to start: Codex has cloud runs, Claude Code has cloud runs, Cursor has background agents, Devin is still around, and generic sandboxes spin up in seconds. Nobody has to operate their own Kubernetes cluster to get there anymore.
If your system works well enough to run on a cloud runner, you have basically infinite compute. As many tickets in parallel as you want, no machine to die under them, and the ceiling stops being physical the moment you admit most of the fleet doesn’t need to be within arm’s reach.
Local is for feedback loops, cloud is for delegation. Early exploration and UX work stay on my machine, because the loop is interactive and I’m testing and correcting every few minutes. Work with clear acceptance criteria and a hands-off setup goes to the cloud, and most of my backlog-clearing runs qualify.
%%{init: {"look": "handDrawn"}}%%
flowchart TB
T["A piece of work"] --> Q{"Interactive loop, or<br/>clear acceptance criteria?"}
Q -->|"testing every few minutes"| L["Local · feedback loops<br/>(exploration, UX)"]
Q -->|"hands-off, well specified"| C["Cloud · delegation<br/>(backlog clearing)"]
The same split decides where each ticket runs, and it’s what stops the machine from being the ceiling.
Run a fleet long enough and you also get price conscious, because you can’t put the most expensive model on every ticket. Cloud agents increasingly build that optimization in: auto modes that route to a cheaper model when the task allows, and open-source models that are genuinely good now. You don’t need the best model every time.
Push the fleet into the cloud and the queue re-forms downstream, and I think I know where. When you can plan an epic in the morning and destroy it by the end of the day, work that used to take a sprint and several people, the question becomes whether you actually have 25 epics per person worth of planned work every month. The bottleneck leaves engineering and shows up in the product world, in knowing what to build. That arc is its own post.
The job is watching where the queue forms
My role has settled into scheduler and reviewer, and if you’re shipping software, yours probably will too. People talk about dark factory patterns, spec in and working code out, and a factory is a simple thing underneath: workers that do the work, a tracker, and some way to validate what comes off the line. Architecting that factory and operating it are different jobs.
%%{init: {"look": "handDrawn"}}%%
flowchart LR
Spec["Spec in<br/>(tickets)"] --> Workers["Workers<br/>(agents)"]
Workers --> Validate["Validation<br/>(proof of each criterion)"]
Tracker["Tracker"] --- Workers
Validate --> Out["Working product out"]
The skills I’ve built, clearing the backlog, speccing the tickets, keeping the tracker honest, are pieces of a simple factory, and that’s why running it feels easy now: this is what it looks like when you operate a factory you built.
The one thing I refuse to give up while the constraint keeps moving is knowing how the system bolts together: if someone asks me how it works and I have to go ask the agent, what am I doing? Reading proofs instead of diffs is how I keep that knowledge at fleet speed, not just how I catch bugs.
Wherever the queue forms next, in the bill, in the planning, or somewhere I haven’t guessed, I expect to find it the way I found the first three: watching where work piles up while everything upstream reports green.
Discover more from zach wills
Subscribe to get the latest posts sent to your email.