Blog

When the deal stalls on a boring question: from 11% to 100% success under load

August 12, 2026 · Fabership

A founder selling into enterprise described the exact moment his pitch dies, in r/startups on July 30, 2026:

"This is the exact moment move fast products die in enterprise: someone asks a boring compliance question and the whole pitch stalls. Worth having a one-page data flow diagram ready before the first demo, not after." (source: https://www.reddit.com/r/startups/comments/1va32qm/comment/p0nw7wf/)

The question is almost never dramatic. Where does the data live. Who can read it. What happens when all forty people at the customer log in on the same Monday morning. Boring questions, right up to the second you notice your honest answer is a shrug.

Somebody in r/lovable put the shape of it better than I would have:

"This is the scariest category of vibe-coded bug, because the app looks completely fine until someone pokes at it." (source: https://www.reddit.com/r/lovable/comments/1tqcw0c/comment/oojnzsn/)

He was writing about authorization checks that live in the interface instead of the database. The sentence describes load just as well. Your app looks fine at three users, looks fine at ten, and picks the worst possible audience for the first time it stops looking fine.

So last July I went poking at ours. We run a compliance SaaS in production, built the way most of these get built: fast, with a lot of generated code, under a deadline. I wanted a number I could say out loud.

At 25 concurrent users, 11% of requests were succeeding. Not slow. Failing.

The obvious suspect was the database connection pool. That diagnosis was wrong, and how we found the real one is the most useful thing I can hand a founder running an AI-built app in production.

The smoking gun

Halfway through the load test, something stopped making sense: at 40 virtual users, even /health was timing out. Our health check is an async endpoint with zero I/O. It touches nothing. No database, no disk, no network. There is exactly one way an endpoint like that can time out, and it is that the event loop itself is blocked. The server was not overloaded. It was standing in line behind synchronous work that someone (us) had dropped onto the async path.

If you take one debugging trick from this article, take this one: load test the endpoint that does nothing. If it degrades, stop tuning your pool and go hunt blockers.

The 22 blockers

We walked every request path and found 22 places where the app blocked its own loop. They came in five families, and I would bet money that a generated codebase has at least three of them:

  1. OCR calls (pytesseract) running inline in request handlers.
  2. PDF rasterization (PyMuPDF at 150dpi), CPU-bound, inline.
  3. subprocess calls (pdftoppm) waiting synchronously.
  4. Object storage uploads (boto3 to R2/S3), network-bound, called without a thread.
  5. file.read() on uploads inside async handlers.

Each one froze the whole server, for every user, for as long as it ran. One person uploading a PDF stopped everyone else's login. The fix is not exotic: blocking work goes to asyncio.to_thread, or to a worker queue when the job is heavy, and sync I/O never gets called straight from an async handler.

Finding all of them is the hard part, which is why we now enforce it with a test. An AST-based guard walks the routes and services and fails CI if anyone reintroduces sync I/O. Finding them once is a cleanup. Keeping them out is engineering.

What else the audit surfaced

Unblocking the loop exposed the next layer down, and that is typical. Fixes come in strata.

N+1 queries everywhere. One screen made 42 queries where it needed 4. Another made 100 for a 50-company view where it needed 2. A government e-filing generator ran one duplicate-check query per employee, per risk, per sector — roughly ten thousand queries for a mid-sized company. It now preloads the existing keys into a set and checks in memory.

The database had never been analyzed. last_analyze was NULL on all 176 tables. One ANALYZE and one composite index later, the planner woke up.

Pool sizing came off a formula instead of a hunch: 2 × workers × (pool + overflow) ≤ ~90% of max_connections. Counterintuitively, the right move was to size the pool down, from a raw ceiling of 30 connections to 18. That is not more capacity. It is a better failure mode: a request that cannot get a connection now fails in five seconds with a 503 and a Retry-After, instead of hanging for thirty and dragging the workers down with it.

Then two integrity bugs that only load made visible. A middleware swallowed exceptions and silently ran requests twice, writing duplicates. An import rolled back valid rows while reporting them as imported. Load testing is how correctness bugs get caught before your customers catch them for you.

The numbers

BeforeAfter
Success rate under load11% at 25 concurrent users100% at 26
Requests served in the test874, zero timeouts
p50 latency297 ms
Heaviest screen42 queries4
Tables ever analyzed0 of 176176 of 176

The suite that keeps it fixed: 20 query-budget tests that fail if any screen's query count grows with N, plus the AST guards in CI.

I will say the unglamorous part out loud, since this is an engineering article and not a brochure. The tail is still the open front here. Median latency is fine; the slowest requests are where the next round of work goes.

Why the generated code looked fine

Code generators write plausible async code with synchronous landmines inside, because the training data has both styles glued together and no compiler complains about the mix. The generator did not invent this pattern. It learned it from us.

That matters for how you read your own codebase. The problem is not that a machine wrote it. The problem is that nothing in the loop between "it runs" and "it ships" was checking this particular class of mistake, and no interface will surface it for you. The app runs. The demo runs. The first ten users run. It falls over at exactly the moment it starts to count.

Back to the boring question

Here is what the load test bought me, and it was not peace of mind.

It bought a sentence with a number in it. When a buyer asks what happens under concurrent use, the answer is "we load test before release; the last run held 26 concurrent users at 100% success across 874 requests with zero timeouts, at 297 ms median." Whether that number is enough depends on how many people the buyer plans to put on it, which is a conversation, and conversations are what stalled deals need.

You cannot say that sentence from memory or from confidence. It comes from a measurement or it does not exist.

Run the check yourself

We keep a free 13-point production readiness self-check at fabership.com/self-check. You run it on your own system; I do not touch anyone's, and that is a rule here, not a policy paragraph.

Point 11 is this whole article compressed into one question: do you know, from a measurement, how many concurrent users your app survives?

Lucas Augusto is the founder of Fabership, an engineering studio that takes AI-generated SaaS to production.

Find out before your biggest customer does.

One call to scope it. If we are not the right team, you hear it on the call, for free, not after an invoice.

Book a call