Backpressure in agentic teams
Day 24: Backpressure, when the slowest part of your team is a human
Your coding agents can open pull requests faster than any human can read them. If nothing tells them to slow down, the review queue will cope by itself. It won't throw an error. Your reviewers will just stop reading properly, and your dashboard will look better than it ever has.
The green dashboard
At PhoenixDX we started running several coding agents in parallel, and soon after that our delivery dashboard had its best weeks ever. Merges per week went up. Time-to-approval went down. For about two weeks I took that to mean the agents were working and the team was adapting.
Then I picked a handful of merged PRs at random and opened them. Big diffs. One approval each, no comments, approved a few minutes after they were opened. The reviewers hadn't learned to read faster. They'd stopped reading closely, because the queue kept growing and approving was the only thing that made it shorter. I haven't measured this carefully enough to quote numbers, so I won't make any up. The pattern is easy to spot, though. PR volume goes up and approval latency goes down, while comments per PR and review time per line both drop in the same week. That isn't a productivity gain. It's a queue that has quietly stopped enforcing its limit.
This lesson names that failure and gives you an engineering fix for it.
Definition: what backpressure is
Backpressure is how a slow consumer tells a fast producer to ease off. Any pipeline whose stages run at different speeds needs it. Without it, work piles up in front of the slowest stage until something gives: memory, disk, latency or quality. The signal travels against the work. Data flows downstream, and "I'm full" flows back up.
- ▹From the queues day: a queue absorbs short bursts and nothing more. If arrivals stay above the service rate for long, the queue doesn't fix anything. It postpones the failure.
- ▹From the retries day: retries are extra producers. If a producer ignores backpressure and retries harder, it adds to the overload.
Four things a full queue can do
When a buffer hits capacity, the system has four basic options. Someone has to pick the first three on purpose. You get the fourth when nobody picks.
- ▹Block. The producer waits until there's room. A bounded channel in Go (`ch := make(chan Job, 10)`) does this: once it's full, the sender blocks. The cost is a stalled producer, and you can deadlock if you're careless.
- ▹Drop. New work gets rejected or discarded. That's load shedding: an API gateway returning 429 or 503 at saturation, or an LLM inference server turning requests away once its batch queue is full. You lose work, but the loss is visible and the system stays healthy.
- ▹Signal upstream. Tell the producer how much room is left so it can adjust. The TCP receive window is the classic case. Every ACK advertises the receiver's free buffer space, and the sender keeps its unacknowledged bytes in flight within that window. The producer keeps going, at the speed the consumer can handle.
- ▹Silently degrade. Accept everything and do each item worse. Nothing errors, nothing blocks, every metric looks fine. In software it looks like skipping validation under load. With people, it's rubber-stamping.
Human review falls into option four by default, because people are polite and work to deadlines. A reviewer isn't going to send an agent a 429. They'll skim it, approve it and open the next one. Rubber-stamping is an unbounded queue hiding behind a fast approval time. The backlog is still there. It has become unreviewed code in main, and you pay for it later in incidents, reverts and design drift.
Mapping it onto an agentic team
- ▹Producers: the coding agents (Claude Code, Codex, Cursor background agents, your own harness). They're fast, cheap to scale out, and they don't get tired.
- ▹Buffer: the open-PR queue.
- ▹Consumer: the senior reviewer. Their throughput is fixed. Careful review needs focused time, and you can't clone the person.
A back-of-envelope calculation helps here. The figures are illustrative assumptions, not measurements. Five agents open 40 PRs a day between them. One reviewer gets about 6 focused hours a day, and a careful review of a typical agent PR takes about 30 minutes. So they can properly review about 12 PRs a day.
Arrival rate λ = 40 PRs/day
Service rate μ = 12 PRs/day (careful review)
Utilisation ρ = λ/μ = 3.33 (anything > 1 means the queue grows forever)
Option A: hold the bar, queue grows
growth = λ - μ = 28 PRs/day
after 1 work week (5 days): ~140 PRs waiting
Little's Law (L = λW) only holds for a stable queue; with ρ > 1 there is
no steady state, so use drain time instead: a new PR joins behind ~140
others, and at μ = 12 drained/day it waits ~140/12 ≈ 11.7 working days
before anyone reads it (and the wait keeps growing).
Option B: keep the queue flat, depth drops
to drain 40/day in 360 focused minutes -> 9 min per PR
9 / 30 = 30% of the review depth you consider 'careful'
Option C (the fix): make effective λ ≤ μ upstream
fewer, smaller, pre-verified PRs, so each one needs less of the reviewer's time,
and dispatch stops when the window is full.Option B is the silent degradation from the last section. Most teams end up there without ever deciding to. The arithmetic says something has to give, so the real decision is what gives. Don't let it be review depth.
Why 'just throttle the agents' is clumsy
Most people's first move is a hard cap, something like "each agent can open at most 3 PRs a day". That's rate limiting a client without telling it why. The producer has nothing to adapt to. It keeps writing the same 600-line PRs, just fewer of them. The reviewer still gets diffs that are too big, and the cap is wrong every time the reviewer has a light day or a heavy one.
TCP has a better model. The receiver advertises a window and the sender changes what it does to fit. For agents, that means making review capacity something they can see. An agent can read it as an input and decide for itself: write a smaller diff, bundle trivial changes, verify more of its own work, or wait. Agents suit this well because pausing them costs almost nothing and they follow explicit policies reliably. A cap only says no. A window tells the producer how much room is left so it can plan around it.
Moving backpressure upstream: five patterns
- ▹1. A review-capacity budget, checked at dispatch. Count capacity in review-minutes, not PRs. Each reviewer publishes a daily budget, and each PR gets an estimated cost based on diff size, risk tier and files touched. The orchestrator checks what's left before it starts a task, not once the PR already exists. Holding back work that hasn't started costs nothing.
- ▹2. A diff size limit. If a change goes over X lines, the agent splits it into stacked PRs before opening any of them. Pick X based on what your reviewers can read carefully in one sitting, which is often a few hundred lines. Agents are good at splitting when you ask. People usually skip it because it's tedious.
- ▹3. Self-verification gates that earn priority. A PR that comes with passing tests, a short spec diff (intended behaviour vs. actual) and a risk summary (what might break, what wasn't tested) is cheaper to review, so move it up the queue. Agents get paid in queue position for work that saves the reviewer time.
- ▹4. Batching for low-risk changes. Dependency bumps, lint fixes, doc typos and generated code all go into one unit per day, reviewed once against a lighter checklist. It's Nagle's algorithm again: combine small writes so you don't pay overhead on every one.
- ▹5. A visible 'queue full, task parked' state. When the gate says no, the task appears on the board as parked, with the reason and its place in line. A silent wait looks like a hung agent. A visible park shows exactly where the bottleneck is, and that's what an EM needs for staffing and prioritisation decisions.
# Sketch of a dispatch gate. Cost is in reviewer-minutes, not PR count.
MAX_DIFF_LINES = 400
def estimate_review_cost(task) -> int:
base = task.est_lines / 15 # rough: ~15 lines per minute read carefully
risk = {"low": 0.5, "medium": 1.0, "high": 2.0}[task.risk_tier]
discount = 0.7 if task.self_verified else 1.0 # tests + spec diff + risk summary
return int(base * risk * discount)
def dispatch(task, reviewer):
if task.est_lines > MAX_DIFF_LINES:
return split_into_stack(task) # producer adapts its shape
if task.risk_tier == "low":
return add_to_daily_batch(task) # amortise review overhead
cost = estimate_review_cost(task)
if reviewer.budget_remaining_today() < cost:
return park(task, reason="review window full",
position=queue_position(task)) # visible, not silent
reviewer.reserve(cost)
return start_agent(task)Yes, the cost model is crude. That's fine. TCP's window isn't a precise measure of receiver capacity either. What matters is that the signal exists, the producer can see it, and the producer acts on it.
What to measure so silent degradation can't creep back in
Option four never throws an error, so you have to go and look for it. Track how deep reviews are, not only how fast.
- ▹Comments per 100 changed lines. If it keeps falling while PR volume rises, that's the textbook sign of rubber-stamping.
- ▹Review time relative to diff size. If a 50-line PR and an 800-line PR are both approved in about 4 minutes, nobody read the 800-line one.
- ▹Post-merge defect rate and revert rate, tagged by author (agent or human) and by diff size. These lag. They're where the bill shows up.
- ▹Approval latency as an alert. When it drops suddenly while volume climbs, investigate before you celebrate. Faster approvals are a symptom of silent degradation. They don't show that everything is fine.
Put depth and speed on the same dashboard panel. A team that only shows throughput and latency is rewarding exactly the failure this lesson describes.
The uncomfortable part for engineering managers
The bottleneck is doing its job. In a well-designed pipeline, the slowest stage should be the one where judgement happens, and for agent-written code that's human review. If careful review is what limits throughput, the system is working as designed. If throughput went up because review got shallower, you've taken on debt you haven't paid yet: code nobody understands, merged at agent speed.
So the questions change. "How do we review faster?" becomes "How do we get more out of each review?" (smaller diffs, pre-verified PRs, batching). "Do we need another senior reviewer?" stops being a gut call, because the parked tasks give you the evidence. If you want more throughput, make the reviewer's time count for more or add reviewers. Don't make the reviews thinner.
Bridge to Day 25
Backpressure deals with a consumer that's slow but healthy. Tomorrow is the harder case: a dependency that keeps failing while agents keep retrying it. Think of a flaky test suite, a rate-limited model API or a broken staging environment. We'll cover circuit breakers, and how agent retry loops grow into retry storms that take down the very thing they're waiting on.
Extend your knowledge
- ▹RFC 9293 (TCP), the flow control section. Focus on the receive window. It's the cleanest model of capacity the producer can see.
- ▹Marc Brooker on queues, retries and backoff, on his blog and in his Amazon Builders' Library article 'Timeouts, retries, and backoff with jitter'. Read them alongside David Yanacek's Builders' Library pieces 'Using load shedding to avoid overload' and 'Avoiding insurmountable queue backlogs'.
- ▹The Reactive Streams specification, which makes backpressure part of the protocol: the consumer requests n items and the producer never sends more than n.
- ▹Exercise: export a month of PR data from your repo and plot comments per 100 LOC against weekly PR volume. If the lines cross the wrong way, you've found silent degradation.
Discussion
Chat with Chi Cong (AI) about this article. Your conversation is private to you — you can publish a summary for others when you're done.