Skip to main content

Scaling software built with AI: where the real bottlenecks are

· 26 min read
Pere Pages
Software Engineer
A software pipeline where AI agents produce code very fast at one end and the work piles up in front of a few slow stages run by people

Imagine a team where artificial intelligence (AI) agents write most of the code, and then imagine it ten or a hundred times bigger. Writing code stops being the slow part. This post walks through the stages that become slow instead, from reviewing pull requests to talking to other systems, and proposes the best fix for each one.

The gist​

When agents write the code, the work waits for people instead: to decide what to build, to review the changes and to trust the tests. Spend human attention where it changes the result, and let automatic checks carry the rest.

  • Write a short spec with examples before any code. A spec is a few lines that say what the change must do. Its examples become the tests, so "done" means something a machine can check.
  • Review by risk, not by habit. Changes to money, login, personal data or interfaces other teams use get a full human review. Small, low-risk changes merge on their own when every check passes.
  • Keep the tests separate from the code they check. Tests written from the spec, not by the agent that wrote the code, are the main guard once people stop reading every line.
  • Start with the cheap fixes. A file of instructions every agent reads, a queue that tests changes together before they merge, and a locked-down space for agents to work in cost little and remove a lot of waiting and risk.
  • Make every release small and easy to undo. Release to a few users first, and go back automatically when errors rise.

The picture: a pipeline where writing code costs almost nothing​

Software reaches users through a chain of steps. Someone decides what to build. Someone writes the code. Someone checks that it works. The change is joined with everyone else's work, released, and then kept running. Each step is a stage of a pipeline, and work moves from one stage to the next.

A pipeline has one simple rule: it can never go faster than its slowest stage. If one stage handles ten changes a day, the whole pipeline delivers at most ten changes a day, however fast the other stages are. Making a fast stage even faster changes nothing. The extra work just waits in a queue in front of the slow stage.

AI coding agents (programs that use a large language model, or LLM, to read a codebase, write code and run commands on their own) make one stage, writing the code, much faster. Picture it at three sizes:

  1. One developer with one agent. The developer asks, the agent writes, the developer reads and fixes. It feels fast, because the person who checks the work is the same person who asked for it.
  2. A team of ten, each running several agents. Now there are dozens of changes a day. Each one needs a reviewer, a test run and a merge. People start to wait for each other.
  3. A company of a hundred people with hundreds of agents. Thousands of changes a week. Writing code is no longer the limit at all. Everything around the code is.

The data already shows this shift. Faros AI measured about 10,000 developers and found that on teams with heavy AI use, each developer finished 21% more tasks and merged 98% more pull requests. But the time spent reviewing pull requests went up by 91%, and the company-level results showed no clear improvement[1]. Google's 2025 DevOps Research and Assessment (DORA) report found the same pattern from a different angle: more AI use now comes with faster delivery, but also with less stable delivery. Its summary is that "AI doesn't fix a team; it amplifies what's already there"[2].

There is also a warning about feelings. In a controlled study by the research group METR (Model Evaluation and Threat Research), experienced open-source developers took 19% longer to finish real tasks when they could use the AI tools of early 2025. Afterwards, they still believed the tools had made them 20% faster[3]. So the sense that "everything is faster now" is not a good guide. You have to look at where the work waits.

Where the pipeline rule comes from

The idea that a system is only as fast as its slowest part has two well-known names. In computing it is Amdahl's law (Gene Amdahl, 1967): speeding up one part of a task gives limited total gains if the other parts stay the same[4]. In management it is the theory of constraints, from Eliyahu Goldratt's 1984 novel The Goal: find the one constraint, and focus all improvement there[5].

The rest of this post follows the pipeline in order. For each stage: the real problem, why it gets worse as the team grows, and the best fix. After the stages come two problems that touch every stage: the team's knowledge, and what agents are allowed to do.

Knowing what to build​

The first slow stage is a human decision: what exactly should this change do?

The real problem. An agent does exactly what the request says, and fills every gap with a guess. A person who writes code slowly has days to notice that the request was unclear and to ask. An agent finishes in minutes, so the unclear request turns into finished code before anyone notices. The result is the wrong thing, built quickly and well.

Why it gets worse at scale. Every change needs someone to decide what "done" means. With ten times more changes, the people who make product decisions (a product manager, a tech lead, a domain expert) get ten times more questions. They become a queue. Agents waiting for an answer either stop or guess, and both are expensive.

The best fix: make the intent a written, checkable thing.

  • Write a short spec for every change before any code. A spec (a specification) is a few lines: the problem, what is out of scope, and three to five concrete examples of the expected behaviour ("given a cart with two items, when the user removes one, the total shows one item"). It takes ten minutes and removes most of the guessing.
  • Turn the examples into tests first. The examples in the spec become automatic tests before the agent writes the feature. Now "done" has a clear meaning: the tests pass.
  • Give each product area one person who can decide. Decisions should not need a meeting. One named owner per area answers questions the same day.
  • Use cheap code to decide, not only to build. When nobody knows what users want, ask an agent for two or three throwaway prototypes and show them to real users. Code that costs almost nothing is a fast way to answer a product question.

With a clear intent, the change gets written. The next question is whether anyone has looked at it.

Reviewing the changes​

This is the bottleneck most teams feel first: pull requests wait in a long line for a human to read them. A pull request (PR) is a proposed change that a teammate reviews before it joins the main code.

The real problem. Reading code is slower than writing it, and for agents the gap is huge. An agent writes 500 lines in two minutes. A careful human reviews maybe a few hundred lines an hour, and only for a few focused hours a day. AI also makes PRs bigger: Faros measured a 154% increase in average PR size on teams with heavy AI use[1]. When reviewers face more and bigger PRs, one of two things happens. Either the queue grows and everyone waits, or reviewers start approving without really reading. The second is worse, because the review still looks done.

Why it gets worse at scale. The number of reviewers grows slowly, because they are people. The number of changes grows with the number of agents. A team that was fine with a ten-PR queue cannot survive a two-hundred-PR queue.

The best fix: review the risk and the evidence, not every line.

Not every change deserves the same attention. A typo fix in the documentation and a change to the payment code are not equal. Sort each PR by risk, and spend human attention only where it changes the outcome:

A public API (application programming interface) is any interface that code outside your team depends on, so a mistake there breaks other people. A database migration is a change to how stored data is shaped, which is hard to undo.

On top of the risk tiers:

  • Keep PRs small. Set a size limit (for example 400 changed lines) and ask agents to split larger work into a series of small PRs, each building on the one before. Small PRs are reviewed faster and more carefully.
  • Let an AI reviewer go first. A second model, with different instructions from the one that wrote the code, catches the mechanical problems: style, missing error handling, obvious bugs. The human then spends their time on the questions only a human can answer: is this the right change, and is it safe?
  • The person who started the agent reviews first. A human who launches an agent owns its output. They read the PR before asking a teammate to. This removes most of the noise before it reaches the shared queue.
  • Put the evidence in the PR. Every PR opens with its spec, the test results and, for anything visible, a link to a preview environment or screenshots. The reviewer checks evidence instead of running the code in their head.

The tiers only work if the automatic checks can be trusted. That is the next bottleneck.

Trusting the tests​

When people stop reading every line, the tests become the main guard. The question is whether a passing test suite means the code is right.

The real problem. If the same agent writes the code and the tests, both come from the same understanding of the task. If that understanding is wrong, the code is wrong and the tests check for the same wrong thing. Every test passes, and the bug reaches users. Agents also have a known habit: when a test fails, the quickest way to make it pass is sometimes to change the test, not the code.

There is a second, more practical problem: continuous integration (CI), the system that runs every test on every change, becomes slow and expensive. A test suite that takes 40 minutes was acceptable for ten PRs a day. For two hundred, it is a queue of its own. And a flaky test (one that sometimes fails for no real reason) used to cost one re-run a week. At scale, flaky tests block dozens of changes a day, and people learn to ignore failing tests.

Why it gets worse at scale. More changes mean more test runs, more chances for a flaky failure, and more code that nobody has read. The tests carry more of the trust exactly when they get less human attention.

The best fix: make the tests independent from the code they check, and make them fast.

  • Write the acceptance tests from the spec, separately from the code. A human, or a different agent that sees only the spec, writes them. The code-writing agent may not edit them. Now the tests and the code come from two different readings of the task.
  • Use property-based tests for important logic. Instead of a few examples, a property-based test states a rule ("sorting a list never changes its length") and the tool tries hundreds of random inputs to break it. It finds cases neither the human nor the agent thought of.
  • Use mutation testing to test the tests. A mutation testing tool changes the code on purpose (it turns a > into a >=, for example) and checks that at least one test fails. If no test fails, the tests were not really checking that line.
  • Make CI fast and reliable. Run only the tests affected by a change, cache results, and run in parallel. Move any flaky test out of the blocking suite the same day it is found, with an owner to fix it.

Once a change is reviewed and tested on its own, it has to live together with everyone else's changes.

Merging parallel work​

Many agents working at the same time create a problem humans rarely had at this volume: changes that are each correct, but break when they are joined.

The real problem. A text conflict is easy: Git (the version control tool) sees that two changes edited the same line and stops. The dangerous case is a semantic conflict, where the two changes touch different lines but break each other's meaning. Agent A renames a field in one file. Agent B, at the same time, adds new code in another file that uses the old name. Both PRs pass their tests alone. Merged together, the build breaks.

Why it gets worse at scale. The chance of two changes colliding grows much faster than the number of changes, because every new change can collide with every other change open at the same time. With a hundred agents working on the same codebase, the main branch breaks many times a day, unless something prevents it.

The best fix: test changes in the order they will be merged, and split the work so agents rarely meet.

  • Use a merge queue. Approved PRs do not merge directly. They join a queue, and each one is tested on top of the latest main branch plus the PRs ahead of it. Only changes that pass in that exact combination are merged[6]. The main branch keeps passing its tests.
  • Split the work by module, not by layer. Give each agent a whole slice of the system (one module, with its own data and its own tests) instead of "all the database code" or "all the screens". Two agents in different modules rarely collide.
  • Draw clear boundaries between modules. Each module exposes a small, stable interface and hides the rest. A change inside a module cannot break code that only uses the interface. This was good design before AI. With many agents, it is what keeps parallel work possible.
  • Merge unfinished work behind feature flags. A feature flag is a switch in the code that keeps a new feature turned off until it is ready. Agents can merge small pieces early and often, instead of keeping long-lived branches that grow more different over time and collide at the end.

Inside one codebase, all of this is under the team's control. The next bottleneck is not.

Connecting to other systems​

Almost every real product talks to systems the team does not own: a payment provider, an email service, a partner's API, another team's service inside the same company. These connections run at the speed of the other side, not at the speed of your agents.

The real problem. An agent can learn your codebase by reading it, but it cannot read the other system. It only sees the documentation, which is often old or incomplete. Real external systems are also hard to test against: they need credentials, they limit how many calls you can make, some calls cost money, and you cannot reset them to a clean state between test runs. And when you need a change from the other side (a new field, a higher limit, an answer to a question), you wait for people who work at human speed, with their own priorities.

Why it gets worse at scale. Your side now changes ten times faster, but the other side does not. Every integration becomes a place where fast work waits for slow answers. Breaking changes also become more likely, because more of your code is touching the integration more often.

The best fix: turn each external system into a contract you can test locally.

  • Put one adapter in front of each external system. An adapter is a small module that is the only place in your code that talks to that system. The rest of the code, and every agent, uses the adapter's simple interface. When the external system changes, you fix one place.
  • Write contract tests. A contract test records what your code expects from the other system (which calls, which fields, which answers) and checks both sides against that record. Tools such as Pact make this a normal part of CI[7]. Agents then test against the contract, quickly and without credentials, and you find out about a breaking change before it reaches users.
  • Generate client code from the API description. Many APIs publish a machine-readable description, for example in the OpenAPI format. Generating the client from it gives agents exact, typed information instead of guesses from old documentation.
  • Ask for sandbox accounts early. A sandbox is a test copy of the external system that you can call freely. Getting one usually takes days of human emails, so start that conversation on the first day, not when the code is ready.
  • Write agreements between internal teams down as contracts too. The same rule applies inside the company. If team B's service is a dependency, the agreed interface lives in a file both teams test against, not in a chat message.

Contracts help agents with systems outside the code. The next bottleneck is about knowledge that is not in any code at all.

Keeping the team's knowledge where agents can read it​

A team knows many things that are not written in the code: why a library was chosen, which approach was tried and failed, which customer needs special treatment. Agents only know what they can read, and most of this knowledge is in people's heads.

The real problem. Each agent session starts without memory of the team's past decisions. An agent that does not know "we stopped using that library because of a memory leak" will happily add it back. A human reviewer catches it once. A hundred agents make the same mistake a hundred times, and the reviewers become the team's memory, one comment at a time.

There is a second problem on the human side. When agents write most of the code, the humans who own it understand less and less of it. When something breaks at night, the person on call (the engineer who answers problems outside working hours) faces code they have never read. This is a slow, invisible cost, and it grows every week.

Why it gets worse at scale. More agents repeat the same missing knowledge more often. Without one written source of truth, the codebase also drifts into many styles, because every agent session guesses the conventions on its own.

The best fix: write the knowledge down once, in the repository, and turn repeated rules into checks.

  • Keep an instructions file for agents in the repository. Files such as AGENTS.md or CLAUDE.md hold the conventions every agent reads at the start of a session: how to run tests, which patterns to use, what never to do.
  • Record decisions as architecture decision records. An architecture decision record (ADR) is a short file that says what was decided, why, and what was rejected. Agents and new people read it instead of asking.
  • Turn every repeated review comment into an automatic check. If a reviewer writes the same comment three times, it becomes a lint rule (an automatic code style check) or a test. The rule then applies to every agent, every time, with no human attention.
  • Keep humans reading the design, not every line. Owners regularly read the module structure, the interfaces and the decision records, even when they no longer read every PR. That keeps a real understanding of the system, which is exactly what someone on call needs at night.

Good knowledge reduces mistakes, but some mistakes will still reach users. That is the next stage.

Running it in production​

Every release is a small risk. When the number of releases grows, so does the number of incidents, unless each release gets safer.

The real problem. The DORA report is clear on this: AI adoption is still linked with less stable software delivery, and its authors explain why: "an increase in change volume leads to instability" when testing, version control and feedback loops are weak[2]. Production is where the costs of every earlier bottleneck finally arrive: the unclear spec, the rushed review, the test that checked the wrong thing.

Why it gets worse at scale. The on-call engineers do not multiply with the agents. More changes per day means more possible causes when something breaks, and finding which of fifty releases caused a problem is slow work.

The best fix: make every release small, observable and easy to undo.

  • Release gradually. A canary release sends a new version to a small share of users first (for example 1%), and only continues if the error rate stays normal.
  • Roll back automatically. If error rates or response times get worse after a release, the system goes back to the previous version without waiting for a human.
  • Keep feature flags for risky behaviour. A feature can be turned off in seconds, without a new release.
  • Measure the change failure rate. This is the share of releases that cause a problem in production. If it goes up as volume goes up, slow down the volume until the safety measures catch up. Speed that ends in incidents is not speed.

The last bottleneck is about what agents are allowed to do along the whole pipeline.

Giving agents only the access they need​

Agents run commands, read files and call services. The more they can do, the more damage one wrong or manipulated action can cause.

The real problem. The usual protection is a human who approves each risky action. That works for one developer and one agent. With hundreds of agents asking for approval all day, people stop reading the prompts and just click "yes". This is approval fatigue, and it is the review bottleneck again, in a more dangerous place.

There is also a real attack. Prompt injection is when text the agent reads (a web page, an issue, a document) contains hidden instructions that the model follows. Simon Willison describes the dangerous combination as a "lethal trifecta": an agent that has access to private data, reads untrusted content, and can send data out. With all three, an attacker can trick it into leaking your data[8].

Why it gets worse at scale. More agents means more credentials, more access and more untrusted content read every day. A single badly configured agent template, copied to a hundred agents, becomes a hundred agents with the same weakness.

The best fix: replace human approvals with rules the system enforces.

  • Run agents in a sandbox by default. A sandbox limits which files an agent can change and which network addresses it can reach. Inside it, the agent works freely and needs no approvals.
  • Give each task only the access it needs. Least privilege (giving a task the smallest access that still lets it work) means an agent fixing a user interface bug has no access to production databases. Use short-lived credentials that expire when the task ends.
  • Never give one agent all three parts of the trifecta. If an agent reads untrusted content, it does not also get private data and a way to send it out.
  • Write the rules as policy, and log everything. Allowed actions live in a configuration file that is reviewed like code, and every action is recorded. Humans then review the rules once, not each action a thousand times.

Which bottlenecks really hurt​

The eight bottlenecks are not equal. Two things decide how much each one matters: how much it slows a large team down, and how hard it is to fix. The table summarises both, plus the first move for each.

BottleneckHow much it slows a large teamHow hard to fixFirst move
Reviewing the changesSevereMediumRisk tiers and a PR size limit
Trusting the testsSevereMediumAcceptance tests written from the spec, separately from the code
Knowing what to buildSevereHardA short spec with examples for every change
Running it in productionHighMediumGradual releases with automatic rollback
Merging parallel workHighMediumA merge queue, then module boundaries
Connecting to other systemsHighHardOne adapter and contract tests per system
Team knowledgeHighEasyAn agent instructions file and decision records
Agent accessModerate, with rare disastersFairly easySandbox by default, least privilege

The same picture as a chart: the top-left corner is where to start, because the pain is high and the fix is cheap.

Look at the three severe ones together: knowing what to build, reviewing and trusting the tests. They are not three separate problems. They are the same problem: the amount of human judgement a team has each day is limited, and AI does not make it bigger. Merges, integrations and production incidents are where a shortage of that judgement shows up later.

What this means for the people​

Hiring more reviewers does not fix a judgement bottleneck, and neither does asking the same people to read faster. The fixes in this post all follow one pattern:

  1. Move human attention to the start and to the risk. People decide the intent, write the examples, and look closely at the few changes that can really hurt. They stop reading every line of every change.
  2. Make machines produce the evidence. Tests written from the spec, contract tests, merge queues, canary releases and policies all answer the question "is this safe?" without a person checking each case.
  3. Write the team's knowledge down once. Every rule that lives in a file or a check is a rule no human has to repeat.

When writing code becomes almost free, the valuable work of a software team becomes deciding what to build and proving that it works. Teams that organise around that will get the speed AI promises. Teams that only add more agents will get a longer queue in front of the same few people.

References​

  1. Faros AI, The AI Productivity Paradox Report 2025
  2. Google Cloud, Announcing the 2025 DORA Report: State of AI-assisted Software Development
  3. METR, Measuring the Impact of Early-2025 AI on Experienced Open-Source Developer Productivity (July 2025)
  4. Wikipedia, Amdahl's law
  5. Wikipedia, Theory of constraints
  6. GitHub Docs, Managing a merge queue
  7. Pact documentation, Introduction
  8. Simon Willison, The lethal trifecta for AI agents: private data, untrusted content, and external communication (June 2025)