Why AI Coding Agents Write Slow Code
Correctness is visible in a diff and performance is not. Six patterns account for nearly all agent-written slowness, and each has a shape you can scan for.
AI coding agents write slow code for a specific and predictable reason: they optimise for code that obviously works, and the fastest way to make something obviously work is to do it in the simplest possible loop. Correctness is visible in a diff. Performance is not. So you get a function that passes every test, reads beautifully, and issues four hundred database queries where one would do.
Here are the six patterns that account for nearly all of it, why each one comes out of the model, and what to look for in review.
1. The N+1 query
The single most common one, by a wide margin.
// what you get
const orders = await db.order.findMany({ where: { userId } })
for (const order of orders) {
order.customer = await db.customer.findUnique({ where: { id: order.customerId } })
}Fifty orders, fifty-one queries. The agent produced it because "fetch the orders, then get each customer" is the correct description of the task, and it wrote the description down.
The fix is a join or a batched fetch, and every ORM has one. What matters for review is the tell: any `await` inside a `for` or `map` over data from another query. That shape is almost always wrong, and it is easy to grep for.
2. Loading the whole table to count or filter
users = db.query(User).all()
active = [u for u in users if u.status == "active"]
return len(active)Works perfectly on the fifty rows in the development database. Falls over at two million.
This appears because filtering in application code is more readable, and because the model has no idea how big your tables are. It has never seen your production row counts and will never ask. Anything that pulls a full collection and then narrows it in memory should push the narrowing into the query.
3. Recomputing inside the loop
for (const item of items) {
const rates = await fetchExchangeRates() // same result, every iteration
total += item.price * rates[item.currency]
}The agent placed the fetch where it is used, which is the clearer place to put it if you are reading the loop body in isolation. Hoisting invariant work out of a loop requires reasoning about what changes between iterations, and that is precisely the kind of whole-function reasoning that gets lost when the model is producing code line by line.
4. Missing indexes on generated schemas
Ask an agent to design a schema and you get correct types, sensible names, working foreign keys, and indexes only where the ORM adds them automatically. Nothing in the schema definition signals which columns your application filters and sorts by, because that information lives in the query code, which the model was not looking at when it wrote the schema.
This one is the most expensive because it is invisible until traffic arrives, and by then the table is large enough that adding an index is an operational event. If you generate schemas this way, the follow-up prompt is worth memorising: given these queries, which indexes are missing. The broader technique is in prompting AI to design a database schema.
5. Sequential awaits that had no reason to be sequential
const user = await getUser(id)
const settings = await getSettings(id)
const plan = await getPlan(id)Three round trips where one `Promise.all` would do. None of these depend on each other, and the model wrote them in the order a person would say them out loud.
Easy to spot once you look for it: consecutive awaits where no line uses the result of the line above.
6. Quadratic work hidden inside a helper
const missing = wanted.filter(w => !existing.some(e => e.id === w.id))Reads as one line. Is a nested loop. At a thousand items on each side that is a million comparisons, and the code gives no visual hint that anything expensive is happening. A `Set` of existing ids turns it linear.
Models produce this constantly because `.some()` inside `.filter()` is idiomatic, compact, and correct. Idiomatic and correct is what they are optimising for.
Why the model writes slow code
Three reinforcing reasons, worth understanding because they tell you what will and will not fix it.
**The training data skews small.** Most published code examples operate on a handful of items, because examples are written to be readable. Code that is careful about scale is longer, uglier, and less likely to appear in a tutorial. The model learned the distribution it was shown.
**Correctness has a reward signal, performance does not.** Tests pass or fail. Performance degrades gradually and only under conditions that do not exist in development. Nothing in the feedback loop the agent operates in punishes a slow implementation.
**It cannot see your data.** It does not know whether `orders` holds three rows or three hundred thousand. Faced with that uncertainty, the simple version is the reasonable default, and it is genuinely the right choice much of the time. Premature optimisation is still a real failure mode.
That third point is why "write fast code" is a weak instruction. The model does not lack the ability to write the fast version. It lacks the information that the fast version is needed here.
What to do about it
**Tell it the scale.** The highest-leverage single sentence you can add to a prompt: "this table has about 2 million rows and this endpoint is called on every page load." That converts an unknowable into a constraint, and the output changes immediately.
**Ask for the complexity after the fact.** A second pass costs almost nothing:
For each function you just wrote, state the number of database queries
and the time complexity as a function of input size. Flag anything that
grows faster than linearly, and anything inside a loop that could be
hoisted or batched. Do not rewrite anything yet.Separating analysis from rewriting matters. Asked to optimise directly, the agent tends to rewrite everything and introduce new bugs. Asked to analyse, it produces a list you can act on selectively.
**Make it measurable rather than reviewable.** Query-count assertions in tests catch N+1s permanently and cost one line. A test that fails when an endpoint issues more than five queries is worth more than any amount of careful reading, because it keeps working after everyone stops paying attention.
**Review the shape, not the logic.** Scanning for correctness is slow and you will miss things. Scanning for four specific shapes is fast: awaits inside loops, `.all()` or `.findMany()` with no filter, nested iteration, and consecutive independent awaits. That is a thirty-second pass over any diff.
This fits alongside the other habits that make agent-written code safe to ship, notably prompting AI to review your code before you ship it and catching an agent introducing a security vulnerability. Performance is the third member of that set and the one most often left out.
It is also worth knowing this is not a reason to stop using agents. Every pattern above is one an inexperienced human developer produces too, and it has been in the literature for decades. Donald Knuth's original discussion of premature optimisation makes the point that the 3% of code where efficiency matters is worth finding deliberately rather than guessing at. That advice was aimed at people. It applies unchanged to agents, and it is the reason the answer is a review pass rather than an instruction to always write the clever version.
For where this sits in the wider picture of what these tools do well and badly, see our overview of AI coding tools.
FAQ
Can I just tell the agent to write efficient code?
It helps less than you would expect, because the model does not know which parts of your system are hot. Telling it the specific scale of the specific data it is touching works much better than a general instruction.
Do bigger models write faster code?
Somewhat, and not reliably. Larger models are better at spotting a quadratic pattern when asked to look for one, but the underlying incentive is unchanged: they still produce the simple version by default because the simple version is more likely to be correct.
Should I let the agent optimise its own code?
Only after it has told you what is slow and you have chosen what to fix. An open instruction to optimise tends to produce broad rewrites, which is how a performance fix becomes a correctness regression.
Is this worse than code written by junior developers?
Broadly similar in kind, and it arrives much faster and in much larger volumes. That is the real difference: the same defect rate over ten times the diff, which is why an automated check beats a manual review here.
How did this land?
About the author

Staff Engineer, Platform
Carlo works on the platform that turns prompts into running apps. He writes the engineering deep dives and the changelog notes worth reading.


