Why does my AI-built app slow down or break once real users start using it?

The answer

Your app was tested by you against fifty rows, and three things that are invisible at that size become severe at a real one: queries that load everything, a database call inside a loop, and no indexes. Add concurrency, uploads and free-tier ceilings, and the app does not crash — it gets slowly unusable.

By Muhammad Bilal9 min read

The short version

  • Real users are not a traffic problem. They are a data-volume problem, a concurrency problem and a variety problem, and all three are invisible in a database with fifty rows in it.
  • The characteristic failure is not a crash. It is an app that gets steadily slower while every dashboard still says green — which is why it usually gets noticed by customers rather than by you.
  • Three query patterns cause most of it: loading every row instead of a page, a database call inside a loop, and no index on the columns you filter by. All three are a day's work to fix and none of them are visible before they bite.
  • Some of what breaks is not your code at all — it is a free-tier ceiling. A hard 100-emails-a-day cap stops your signup flow at user 101 with no error on your side.
  • You can reproduce almost all of this in an afternoon by seeding your database with realistic volume and opening the Network tab. Nobody does, which is why it keeps happening.

There is a version of this story I hear almost every month. The app worked. It was demoed, it was launched, the first users came in, and then somewhere between week two and week six it started to feel wrong — a page that takes six seconds, a table that spins, a signup that occasionally does nothing at all. Nothing has errored. Every status page is green. And the AI that built the thing has no idea why, because from where it sits nothing changed.

Nothing did change. The app is doing exactly what it always did. What changed is that the conditions it was built under stopped being true.

The three things "real users" actually means

When people say an app fell over under real users, they almost always assume they mean traffic. Traffic is rarely the issue at this stage — a modern host will serve far more requests than a young product generates. What real users actually bring is three different things.

Volume. Your database had fifty rows in it when you built the app, and they were rows you typed. Now it has thirty thousand, and every query that quietly loads all of them is loading all of them.

Concurrency. You used the app one action at a time. Real users overlap. Two people edit the same record. The same person double-clicks a submit button on a slow connection. A webhook and a user action touch the same row in the same second. None of that ever happened in testing, because testing was one person being careful.

Variety. You always used the app correctly, because you knew how it was supposed to work. Real users press the back button mid-flow, open two tabs, refresh during checkout, paste an emoji into a name field, upload a 40 MB photo from a phone, and abandon things halfway through. Every one of those is a path that was never written.

The three compound. Most of what follows is one of them wearing a specific costume.

It does not crash. That is the problem.

Before the specifics, the single most useful thing to understand about this category is its failure signature.

A crash is a gift. It is loud, it is timestamped, something tells you about it. What actually happens here is degradation: the app keeps working and gets worse. Six hundred milliseconds becomes two seconds becomes six. Nothing errors, so no alert fires. Your host reports 100% uptime, accurately, because every request did eventually return.

The consequence is that you do not find out from your monitoring. You find out because someone mentions it in passing, or more often you never find out at all and simply watch a conversion number sag for reasons nobody can name. An app that is slowly becoming unusable and an app that is fine look identical from the inside.

That is why the checks below are worth doing on purpose, before there is anything to notice.

The three query patterns behind most of it

Nearly all of the volume problem is one of these three, and frequently all three at once on the same screen.

Loading everything instead of a page. The generated code asks the database for every row in the table and then displays the first twenty. At fifty rows this is invisible. At thirty thousand it is a multi-megabyte response travelling to a phone. The tell is a single request in your Network tab with a very large response size and a very long time next to it.

A database call inside a loop. The screen shows twenty items, and for each item it goes back to fetch that item's author, or its status, or its count. Twenty-one round trips where there should be one. This one is genuinely characteristic of generated code, because when you ask for "show the author's name next to each post" the most obvious implementation is a lookup per post and it works perfectly at demo scale. The tell is a burst of near-identical requests firing together, each one fast, adding up to seconds.

No indexes. Every column you filter by, sort by, or join on needs an index, and generated schemas rarely have any beyond the primary key. Without one, filtering is a full scan of the table — which was free at fifty rows and is not at thirty thousand. The tell is a query whose time grows roughly in proportion to how much data you have, which is exactly the shape that makes an app feel like it is "getting slower".

All three are fixable in a focused day, and it is usually the single cheapest large improvement available in an AI-built app. Taking a screen from six seconds to under one changes how the product feels more than most features do.

What concurrency breaks

The volume problems are slow and visible if you look. The concurrency problems are fast, rare and invisible, and they corrupt data rather than delaying it.

The most common one is the double submit. A user on a slow connection taps the button twice because nothing happened the first time, and you get two orders, two accounts, two of whatever it was. Generated code almost never guards against this, because in testing the response was instant and nobody ever tapped twice.

The second is the read-modify-write race. Two operations read the same value, both add to it, and one of the two increments disappears. Credits, counters, stock levels and usage quotas are where this shows up, and the symptom is a number that is quietly, unaccountably wrong.

The third is the webhook arriving twice, which every payment provider will do by design, because they retry until they get an acknowledgement they trust. If the handler is not idempotent, the second delivery grants the subscription a second time or emails the receipt again.

The fixes are unexciting and durable: unique constraints in the database so the second insert is refused rather than accepted, doing the arithmetic in the database rather than in the app, an idempotency key on anything that grants or charges, and disabling the button while a request is in flight — which is the cosmetic half, not the fix.

The limits nobody mentions until you cross them

Some of what breaks is not your code. It is a ceiling on a free plan, and the reason it catches people is that it fails on a threshold rather than gradually. Everything is fine, then one specific thing is not, and there is no error on your side because the refusal happened somewhere else.

The one that hurts most is transactional email. Resend's free tier allows 3,000 emails a month, which sounds generous, but it also enforces a hard 100-per-day cap. If your app sends password resets, magic links, receipts and notifications, a good day takes you past that, and everything after it does not go out. Your signup form will look like it worked.

Supabase's free tier stops at 500 MB of database and 5 GB of egress, allows only two active projects, and — the one that surprises people most — pauses free projects after a week of inactivity. For a side project between launches, that is an app that is simply off one morning. It also has no backups on the free plan, which is a different and worse problem the day you need one.

Vercel's Hobby plan is free and is explicitly for personal, non-commercial use, which most people building a product to charge for have not read. Netlify handles limits differently again: rather than billing you for overage, sites pause at the ceiling. Those are two genuinely different failure modes and it is worth knowing which one you are running on. Sentry's free tier covers 5,000 errors a month, counted per event rather than per unique bug — so a single loop erroring on every page load can consume the month in an afternoon, and then you stop being told about anything.

None of these are unreasonable limits. They are all clearly documented. They just are not documented anywhere near where you were building, and they all fail in the same way: quietly, on a threshold, on a good day.

How to see it before your users do

This is an afternoon, and it finds nearly everything above.

Seed your database with realistic volume — a few thousand rows in your busiest table, and a couple of hundred belonging to one user so you can see what a heavy account looks like. Any script will do; the AI will write it for you in a minute, and this is exactly the kind of visible, verifiable task it is good at.

Then use your own app normally with developer tools open on the Network tab. You are looking for three shapes. A single request with a large response and a long duration is the load-everything pattern. A cluster of many small near-identical requests is the loop pattern. A request whose time is much worse than the same request was against your old test data is the missing index.

Then throttle. Chrome's Network tab has a throttling dropdown; set it to a slow connection and repeat your most important flow. This is where double-submits, spinners that never resolve and unhandled timeouts announce themselves — because on your broadband every response was instant and none of those paths were ever entered.

Finally, open the same flow in two tabs and do the same thing in both. It takes two minutes and it is the cheapest concurrency test that exists.

The order to fix in

Indexes first, because they are close to free and frequently fix most of the perceived slowness on their own. Then pagination on whichever screen shows the most rows. Then the loops, which are the most work and the most satisfying. Then the concurrency guards — unique constraints and idempotency on anything that charges or grants access.

And before any of it, error tracking, if you do not already have it. Not because it fixes anything, but because everything on this list is currently happening to somebody without you knowing, and an hour of setup converts the whole category from anecdote to data. That is also the thing that makes the next incident survivable, which is its own subject in your live app is down: the next hour.

Worth adding: if things also behaved differently the moment you moved from the editor's preview to your real domain, that is a separate problem with its own six causes, and it is covered in why your app works in preview and breaks in production. The two get confused constantly, because both present as "it worked yesterday".

If you would rather have someone else look

If your app is live and something about it feels slower than it used to, that is worth taking seriously even without a specific complaint to point at, because this category never announces itself.

I do a Production-Ready Audit that includes exactly this: the heavy screens profiled against realistic data, the query patterns above identified by name, the ceilings on your current plans checked against your actual usage, and a written fix list in priority order with the honest cost of each. From $499, back in five to seven days.

If you would rather start smaller, send me the slowest screen in your app and roughly how many rows are behind it, and I will tell you which of the three patterns you are looking at. No charge — it is usually recognisable in a minute and it seems unfair to bill for a minute.

The wider picture, including the nine other things that tend to be missing at the same time, is in the ten problems every AI-built app has in production, and the repair work itself is described on the AI SaaS rescue page.

Follow-up questions

What people ask next

How many users counts as 'real users'?

Far fewer than people expect. Most of these problems show up somewhere between the first fifty and the first few hundred active users, and some show up at one — a single user with two thousand records of their own will find your unpaginated list long before a hundred users with ten records each will. The trigger is data volume and concurrency, not headcount.

My app is fast for me. Why would it be slow for anyone else?

Three reasons, usually all at once. Your database has your test data in it and theirs has real data. You are on a laptop on good broadband and they are on a phone on mobile data. And you visit one page at a time while they arrive in overlapping sessions. None of those differences are visible from where you are sitting, which is exactly the problem.

Will upgrading to a paid plan fix it?

It fixes the ceiling problems and none of the code problems. Upgrading removes a hard email cap or a storage limit immediately, and that is worth doing when you hit one. But a query that makes twenty-one database calls where it should make one will make twenty-one calls on any plan you buy — you have just paid more to run the same inefficient thing. Fix the ceilings with money and the queries with code.

Do I need proper load testing?

Almost certainly not yet. Load testing answers 'how many concurrent users before it falls over', which is rarely the question at this stage. The question is usually 'does this screen work with realistic data', and you answer that by seeding your database with a few thousand rows and using the app normally with the Network tab open. That takes an afternoon and finds far more.

What tends to break first?

In my experience, whichever list screen shows the most rows — a dashboard, an admin table, an activity feed. It degrades gradually rather than failing, so it rarely gets reported as a bug. The second is usually something with a hard external limit attached, most often transactional email, and that one fails abruptly and silently on the day you cross the line.

Related reading

Production-Ready Audit

Every table's row-level security reviewed, keys checked and rotated, auth and payments tested — back as a written fix list in priority order.

From $499 · 5–7 days

Muhammad Bilal, Full Stack AI Developer

Muhammad Bilal

Full Stack AI Developer · Faisalabad, Pakistan

I build and rescue production AI SaaS products with Next.js, Supabase, Stripe and Claude. Most of my work is finishing apps that were started with Lovable, Bolt, Cursor or Replit and stalled somewhere between working and shippable.

SF
MS
BK
AS

5.0★ · 100% job success · 35+ projects delivered

All articles · RSS