Your AI-built app is slow because it waits in single file
It was fast when you built it. It is slow now, and you did not change anything. What changed is the data: with twelve rows and one test account, code that waits for one thing before starting the next feels instant. With two thousand rows and real users, the same code is the reason a page takes six seconds.
Loïc Guillebeau8 August 2026Of the ten applications we audited over the last three months, performance came second only to access control as a recurring finding, and it was nearly always the same shape: work that could have happened at once happening one thing at a time. Not an exotic bottleneck, not a missing index, not the hosting. Just code queueing behind itself. (Access control is the other one, if you have not checked that yet.)
Why a model writes it this way
Sequential code is the safest thing a language model can produce. Each line reads correctly on its own, the order is obvious, and nothing can race. If you ask for a dashboard that shows a user, their orders and their invoices, you get three fetches in a row, and it works.
What nobody asked is whether those three things depend on each other. They do not: the orders query does not need the user query to have finished. Running them in sequence costs the sum of three round trips where it could have cost the longest one. That is invisible at ten rows and unmissable at ten thousand.
The two shapes, and how to recognise them
The first is independent work awaited in a row. It looks like this, and it is correct, which is what makes it easy to miss in review.
// three round trips, one after another
const user = await getUser(id)
const orders = await getOrders(id)
const invoices = await getInvoices(id)The second is worse, and more common: a query per row, inside a loop. One request to list the orders, then one more for every single order to fetch its customer. Fifty orders means fifty-one queries. This is the N+1, and an AI builder produces it constantly, because writing it that way is the most natural reading of "show the customer name next to each order".
// 1 query, then 1 more per row
const orders = await db.orders.findMany()
for (const order of orders) {
order.customer = await db.customers.findById(order.customerId)
}Find it in your own repo
You do not need a profiler to find the first pass of this. Grep your code for an await inside a loop, which is the N+1 in almost every codebase that has one.
# awaits inside for / while / map, with a little context
grep -rn -B4 "await" --include="*.ts" --include="*.tsx" --include="*.js" src/ \
| grep -B1 -E "for \(|while \(|\.map\(|\.forEach\("Then look at what your app actually does at runtime. Open the network tab on the page that feels slow and read the waterfall: requests that start one after the other in a staircase are sequential, requests that start together are parallel. If you are on Supabase or a hosted database, its own logs will show you the same story as a burst of near-identical queries a few milliseconds apart.
A staircase in the waterfall is the symptom, not the diagnosis. It tells you the app waits; it does not tell you whether it had to. That answer is in the code, in whether each step needs the one before it.
The fix, and when it is the wrong fix
For independent work, start it all and wait once.
const [user, orders, invoices] = await Promise.all([
getUser(id),
getOrders(id),
getInvoices(id),
])For the N+1, stop looping and ask for everything once, then stitch the two lists together in memory. One query instead of fifty-one.
const orders = await db.orders.findMany()
const ids = [...new Set(orders.map((o) => o.customerId))]
const customers = await db.customers.findMany({ where: { id: { in: ids } } })
const byId = new Map(customers.map((c) => [c.id, c]))
for (const order of orders) order.customer = byId.get(order.customerId)Now the honest part, because Promise.all applied everywhere is its own outage. Parallel is wrong when the work is not independent, and it is dangerous when the thing on the other end has limits.
- If step two needs step one's result, it stays sequential. No amount of Promise.all fixes a real dependency.
- Promise.all rejects as soon as any one of them rejects, and the others keep running unattended. Where a partial result is acceptable, Promise.allSettled is the one you want.
- Five hundred parallel queries will exhaust a database connection pool, and a serverless function that opens them all will take the database down before it takes itself down. Batch in chunks rather than firing everything.
- Third-party APIs rate limit. Turning a polite sequence into a burst is how an integration starts returning 429 in production and nowhere else.
- Fix the N+1 before you parallelise anything. One query that replaces fifty-one beats fifty-one queries running at once, and it does not put the load somewhere else.
What an outside scan cannot tell you
There are tools that will load your deployed URL and report that a page takes six seconds. That is worth knowing, and it is where the useful part ends. From outside, a slow response is a single number: something in there was slow. It cannot see that the number is the sum of forty round trips, that thirty-nine of them were the same query with a different id, or that three of them never needed to wait for each other at all.
That difference is not a detail, it is the whole repair. "This page is slow" sends you looking. "This loop makes one query per row and these three calls are independent" is the fix, already written. The second one only exists if something read the code.
That is the job Grace does: it connects to the repository rather than the URL, so what comes back is the loop, the file, and the line, ranked by whether the slow path is one your users are actually on. The audit is free and reads only. Whether you fix it yourself afterwards, hand the list to a developer, or let Grace open the pull request is your call.

Loïc Guillebeau
Founder, Grace · founder of Beyond the Brackets
Seven years running an engineering agency, ten AI-built applications audited in the last three months. Grace came out of both.
Want to know what else is in there?
Grace reads the repository and comes back with the architecture map, the risks ranked by severity, and a health score. It is free, it is read-only, and it changes nothing.
Keep reading