
Why MVPs Break Under Real Load
Success is the failure mode. The shortcuts that got you to launch are exactly the ones that break when launch works.

"The app is slow" is the most common brief we get, and it is almost never accompanied by a number. The team has a theory — usually the database, sometimes the framework, occasionally the language — and a proposal to rewrite the part they already disliked.
The rewrite is the most expensive possible response to a problem nobody has measured. It is also the one most likely to reproduce the bottleneck in new syntax, because the cause was in the access pattern rather than the code around it. This is the order we actually look in.
An average response time is close to useless. If 95% of requests take 80ms and 5% take 9 seconds, your average looks healthy and one user in twenty is furious. Averages hide exactly the population you are losing.
Look at percentiles — p50, p95, p99 — and look at them per endpoint rather than across the whole app. A slow p99 on a login route is an emergency. The same p99 on a quarterly export is Tuesday.
Nearly every slow request we have profiled has its time sitting in one of four places. Knowing which one changes the entire response.
| Where the time goes | What it looks like | What it usually is |
|---|---|---|
| The database | One endpoint slow, worsens with data volume | A missing index or an N+1 query |
| Something you call | Slow and erratic, unrelated to your own load | A third-party API with no timeout |
| Work on the request path | Slow in proportion to what the user uploaded | A job that should not be synchronous |
| The browser | Server timings fine, users still complain | Bundle size, render blocking, layout shift |
The fourth row is the one engineering teams miss most often, because their own dashboards are green. If your server p95 is 90ms and users still say the app is slow, they are not wrong — you are measuring a different thing from the one they are experiencing.

In a typical product application, the great majority of unexpected latency is query time. Two causes dominate, and both are cheap to fix once you have seen them.
A missing index. Run the query with EXPLAIN ANALYZE. If the plan contains a sequential scan over a large table, you have your answer. Note that this is the same diagnosis we describe for vector search in pgvector versus Pinecone — "the database does not scale" is very often "the index is missing".
An N+1 query. One query fetches 50 rows, then the ORM quietly issues 50 more to load a relation for each. The endpoint is fine with test data and collapses with real data. It is invisible in code review and obvious the moment you count queries per request, which is one line of middleware in most frameworks.
If your own database is fast and the endpoint is still slow, you are probably waiting on somebody else's — a payment provider, a shipping API, a model vendor. Three failures are near-universal here:
Generating a PDF, resizing images, sending email, calling a model, reconciling an order — none of these need to happen before the user gets a response. They need to happen reliably, which is a different requirement, and it is what a queue is for.
Moving work off the request path is usually a larger and more durable win than optimising the work itself. BullMQ versus RabbitMQ versus Kafka covers choosing the broker; the pattern matters more than the product. If most of your endpoints are doing work the user is not waiting for, you are past tuning and into scale and re-architecture.
Users experience the browser, not your API. Google's Core Web Vitals are a reasonable proxy for what they feel, and two of them catch most real complaints: Largest Contentful Paint for how long the page looks empty, and Interaction to Next Paint for how long it feels stuck after a tap.
Field data beats lab data here. A Lighthouse score from your laptop on office wifi tells you very little about a customer on a four-year-old Android phone. Use real-user monitoring if you have it, and if you do not, test on a throttled connection before concluding the front end is fine.
Every hour spent profiling without instrumentation has to be spent again next quarter. OpenTelemetry is the vendor-neutral standard for traces, metrics and logs, and a distributed trace answers in one screen the question that otherwise costs a week: which span consumed the time.
Brendan Gregg's USE method — for every resource, check utilisation, saturation and errors — is still the fastest way to work through infrastructure systematically rather than by hunch. The goal is that the next incident starts with a chart instead of a theory.
A performance engagement under research, debug and analyze starts by instrumenting the system and establishing a baseline, because without one there is no way to prove a fix worked. Then we profile, find the actual bottleneck, fix it, and hand back the before-and-after numbers along with the instrumentation, so the next question is answerable without us.
Most of these engagements end with a smaller change than the client expected. That is the normal outcome of measuring first, and it is the cheapest result available to you.
A sudden change usually has a specific cause: a data volume that crossed a threshold and turned an unindexed query into a sequential scan, a dependency that started timing out, or a deploy that added work to the request path. Compare p95 per endpoint before and after the change rather than looking at an overall average, which will hide it.
Measure per-endpoint percentiles first, then check four places in order: database query time, outbound calls you are waiting on, work on the request path that should be a background job, and the browser. Run EXPLAIN ANALYZE on the slowest query and count queries per request to catch N+1 patterns. Do not optimise anything you have not measured.
Optimise, almost always. Most latency lives in access patterns — missing indexes, N+1 queries, serial network calls, synchronous work that should be queued — and a rewrite reproduces those in new syntax while costing months. A rewrite is justified when the architecture itself prevents the fix, not when the current code is disliked.
As a working target, p95 under 300ms for interactive endpoints and under 1 second for anything heavier. On the browser side, Google's Core Web Vitals thresholds are 2.5 seconds for Largest Contentful Paint and 200ms for Interaction to Next Paint. Measure per endpoint, because one slow export skews a whole-app figure.
Because server monitoring measures a different thing from what users experience. If your API p95 is healthy, the time is going to the browser — bundle size, render-blocking resources, slow interactions — or to a device and network far weaker than your own. Use field data from real users rather than a Lighthouse run on a fast laptop.