The question came in a meeting, from the CEO, and it was a completely reasonable question.
"How do we know how many users the system can handle?"
I knew the answer. The answer was that we didn't know. I also knew that saying "we don't know" out loud, in that room, was going to go badly, so what I actually said was something about horizontal scaling and headroom and how we'd been building with growth in mind. All of which was true. None of which was the number he asked for.
He wasn't being difficult. He had a launch coming, he'd seen what the marketing spend was, and he wanted to know whether the thing we'd built would survive the thing he was about to point at it. That's his job. The uncomfortable part is that it should have been my job to have the answer ready, and I didn't, and I'd been building software for years at that point.
I've since learned that almost nobody has that answer. Including teams considerably better resourced than we were.
Why the question is harder than it sounds
Here's the first thing I'd say to anyone being asked this.
"How many concurrent users" isn't one number. It's a different number for every path through your application, and the paths differ by orders of magnitude.
Your login endpoint might hold two thousand concurrent users without noticing, because it does one indexed lookup and one hash comparison. Your search might fall over at forty, because somewhere in it there's a query doing a sequential scan on a table that was small when someone wrote it. Your dashboard might hold three hundred until the moment the cache expires, and then hold nine.
So when somebody asks for the number, the honest response is "for which flow?" — and that sounds like deflection even when it isn't. The useful version of the answer is a short list: signup holds this, login holds that, the expensive report holds this much less, and here's the one that breaks first.
That list is obtainable. Most teams just never obtain it.
The real reason you haven't load tested
It isn't that you don't believe in it. Every developer I've met agrees load testing is a good idea in roughly the way everyone agrees flossing is a good idea.
It's that load testing has historically required writing a load test.
Which means learning a tool — k6, Locust, JMeter, Gatling, whichever — and they're good tools, genuinely, but each one is a scripting environment with its own concepts. Virtual users, ramp stages, thresholds, checks, think time, data parameterization. You write JavaScript or Python or XML that describes a user's journey, you debug why your script is the thing failing rather than the server, you get it green, and then you start learning something about your application.
That's a day, realistically, for a first useful test. Probably two.
And that day competes with shipping the feature that's actually on the sprint. So it loses. Every time. It loses for a completely rational reason, which is that the feature has a deadline and the load test has a vague sense of prudence.
Multiply by every sprint for two years and you arrive where we were: in a room, being asked a fair question, with no answer.
Removing the script removes the excuse. That's the entire thesis. Not "our load testing is more powerful" — it isn't, k6 in expert hands will do things ours won't. The claim is that a test you can set up in four minutes is a test that actually happens, and a load test that happens beats a better one that doesn't.
So you describe what you want in plain language. "Hit the login endpoint and then the dashboard, ramp up to five hundred users over ten minutes." It asks you for what it's missing — the URL, whether auth is needed, what a successful response looks like — and it won't invent any of it. If it doesn't have a credential it asks rather than guessing, which matters, because a load test silently hammering an unauthenticated 302 is worse than no load test: it produces a number, and the number is a lie.
Developers test with two users, and scaling is a different discipline
This is the part I'd most want the CEO to understand, and also the part I'd most want developers to admit.
When you're building, you test with one user. You, clicking. Maybe two if someone's helping. At that volume, a whole category of defect is not merely undetected but undetectable:
- The N+1 query that fires 200 statements instead of 2. At one user it costs eight milliseconds and you'll never see it.
- The missing index on a table that has four hundred rows in development.
- A connection pool sized at 10, which is infinite headroom until the eleventh simultaneous request.
- A per-request cache that's useless because each user gets their own.
- A lock that's never contended because there's only ever one of you.
- A third-party API call inside a loop, fine at one iteration.
None of these are mistakes exactly. They're code that is correct at the scale it was written for. They become the whole system at two hundred users, and they fail in a way that doesn't look like themselves — it looks like "the site is slow," or latency climbing, or timeouts in something unrelated downstream.
Building and scaling are genuinely different skills, and most of us learn the second one by having a bad week. The value of a load test isn't that it makes you good at scaling. It's that it moves the bad week to a time you chose.
"Will it hold?" is the wrong test. Find the breakpoint.
Here's a mistake I made for a long time: picking a target, testing at it, seeing green, declaring victory.
"We tested at 500 concurrent users and it was fine." Great. What that tells you is that 500 is below your limit. It doesn't tell you whether your limit is 520 or 15,000, and those are completely different operational situations. One means you're one good marketing day from an outage. The other means you can stop thinking about this for a year.
A passing load test at your expected traffic is the least informative possible result. It answers a yes/no question when you needed a number.
What you want is the breakpoint — so you escalate until it actually breaks. CleverQA's capacity sweep ramps up geometrically until something gives, then binary-searches between the last good level and the first bad one to narrow down where the edge actually sits. You get "it degraded between 340 and 360," not "it was fine at 200."
And when a run breaches, it doesn't just leave you a chart. It opens a defect, listing the endpoints that breached, in the same tracker as everything else that's wrong with your system — pushable to Jira, Linear, GitHub or Azure DevOps. A load test result that lives in a dashboard gets looked at once. A load test result that arrives as an assigned work item gets fixed.
And then the subtlety that changes how you read the result: the breakpoint usually isn't where errors start. It's earlier. It's where latency begins climbing out of proportion while throughput flattens out — the knee in the curve. Past that point you're still returning 200s, so every check is green, and every user is having a worse time. Errors come later, sometimes much later. If you only watch the error rate you'll put your capacity estimate well above the point where your product stopped being pleasant to use.
That knee is the number to give the CEO. It's the honest answer to "how many users can we hold," because it's the point where holding them stops being the same thing as serving them.
"How do I optimize the API if I don't know how it behaves under load?"
You don't. That's the trap, and it's why this question is the best one on the list.
Optimization without measurement is superstition. You end up optimizing the thing that looks expensive — the big function, the complicated-looking join — while the actual cost is somewhere unglamorous, like a serializer doing redundant work on every row, or a middleware re-parsing a token on each request.
So the run has to tell you where the time went, not just that it ran out. With CleverQA's agent installed, a run reports per-endpoint and per-query attribution: which endpoints got slow as load climbed, and which specific database queries were the slow ones during that run. That's the difference between "the system degraded at 350 users" and "the system degraded at 350 users and this one query is why."
The second version is a work item, and that's literally what it becomes — a defect with the breached endpoints attached, owned by someone, with a status. Not a screenshot in a channel.
That same agent is the other half of the pair, and this is the bit I'd actually pay for. The load test tells you where the edge is. The agent, left running on your production host, tells you per service — by name — when real traffic starts walking toward it: which service is eating the CPU, which one's memory is climbing and how fast. Not "the box is unhappy," which starts a search. payments-worker is at 94% and rising 6% a minute, which ends one.
Knowing your breakpoint is 340 is interesting. Knowing you're at 280 and climbing, and which service is responsible, is actionable.
A limit worth stating plainly: it tells you which endpoints and queries were slow in a run. It does not tell you which one failed first — there's no temporal ordering of failures, and I'm not going to pretend otherwise, because "what broke first" is exactly the question you'd want answered and we don't answer it yet. You get the list and you use your judgment about the sequence.
"Will it outlive the tech team's promises?"
I want to be fair to the engineers here, because I've been the engineer in this conversation.
When a developer says "it'll handle it," they're usually not covering anything up. They genuinely believe it, on the basis of the architecture being reasonable and nothing having gone wrong yet. That belief is sincere and it is also not evidence.
The gap between those two things is the whole problem, and it isn't solved by asking harder or demanding a firmer commitment. It's solved by converting the belief into a number that someone could check. "I think we're fine" becomes "we degrade at 340 concurrent on the dashboard flow, the constraint is a query in the reporting join, and here's the run."
That sentence ends the argument. Not because it's reassuring — 340 might be alarming — but because it's falsifiable. Nobody has to trust anybody.
And it's repeatable. Schedule the runs, or trigger them from CI on each release with a token, and the number becomes a trend. The question stops being "will it hold" and becomes "did this release make it worse," which is a question with an actual answer. A release that moved your breakpoint from 340 to 180 is a regression, and you'd like to find that out before your users do.
That's why the load-test result feeds the release score. CleverQA gives every release a number from 0 to 100 with a ship-or-hold recommendation, and the stress dimension is one of four inputs alongside uptime, crashes and open defects. So a release that quietly halved your capacity doesn't pass just because all the tests went green — the score drops, and you can gate on it. Friday afternoon, before anybody closes a laptop, somebody looks at a number that knows about the load test.
The expensive alternative: buying headroom instead of knowing
There's a thing teams do instead of measuring, and it's so common it barely registers as a decision. You scale up. Bigger instances, more of them, a generous autoscale ceiling — just in case.
I understand the appeal. It's one afternoon, it requires no new skills, and it genuinely helps with some problems. It's also the most expensive possible way to handle this, for three reasons that compound.
You're paying monthly, forever, for an unknown quantity of insurance. Say your app runs on three instances at a couple of hundred dollars a month each. Doubling that for safety costs you the same again every month, indefinitely — several thousand dollars a year — and here's the part that should bother you: you don't know whether you bought enough. You doubled a number you never measured. Maybe 2x was plenty. Maybe your real problem needed 6x. You can't tell, because the measurement is the thing you skipped. You are paying a recurring premium for a guess, and the guess remains unvalidated for as long as you keep paying it.
Most of what takes you down isn't fixed by hardware. This is the argument I'd lead with if I only had one. Go back to that list of things invisible at two users — the connection pool sized at 10, the uncontended lock, the N+1 query, the third-party call inside a loop. A bigger instance does nothing about a connection pool of 10. Nothing. The eleventh simultaneous request queues on a $2,000-a-month box exactly as it queued on the $200 one. Those failures trip at a concurrency threshold, not a CPU threshold, and the whole over-provisioning strategy is aimed at the wrong axis. You can spend your way to a very fast server that falls over at the same number of users it always did.
Headroom hides your regressions. If you're running at 10x the capacity you need, the release that made everything three times slower is invisible. No alert, no complaint, nothing in the metrics anybody's watching. So it ships, and the next one does too, and you accumulate a year of unnoticed degradation — until you finally grow into your headroom and everything breaks at once, with no recent change to blame and twelve months of candidates. Over-provisioning doesn't just cost money; it buys you the ability to not find out, which you then pay for again later with interest.
Set against that, the arithmetic for measuring is a little absurd. CleverQA's Pro plan is $100 a month. That's less than the difference on most cloud bills between a fleet you measured and a fleet you doubled out of anxiety — and it tends to be a fraction of a single over-provisioned instance.
And the two outcomes are both good. Either the test says you genuinely need more capacity, in which case you buy it deliberately, sized to a real number, rather than guessing at a multiplier — or it says one query in the reporting join is your constraint, you fix it once, and you keep that capacity permanently without renting it. Optimization is capital. Over-provisioning is rent. The fix you make this month is still working next year at no further charge.
The honest caveat: sometimes you do need the bigger servers, and sometimes you need them this afternoon, before anyone has time to measure anything. Scale up — that's what it's for. Just don't let the emergency become the architecture, because the monthly payment outlives the memory of why you started making it.
The thing I wish I'd said in that meeting
Not "we've built it to scale." That's a vibe.
What I'd say now: "I don't have that number. I can have it by Thursday, for the three flows that matter, including where it breaks and what breaks first."
That answer is better in every way, including that it's honest about the present. And the reason I can say it now is that getting the number stopped being a two-day scripting project.
That's the only argument I'm making here. The barrier was never that load testing was unimportant. It was that it was work, and it sat behind a skill nobody on the team had time to acquire, so it waited — right up until the week it stopped being optional, which is always the worst possible week for it.
It's a cheaper question to answer than it used to be. Considerably cheaper than the servers you'd buy to avoid asking it.
CleverQA runs load tests you describe in plain language, escalates until it finds your real breaking point, names the endpoints and queries that got slow on the way there, and files what it finds as tracked work. The same agent then watches your production hosts and tells you which service — by name — is walking toward that limit. Free plan, no credit card: cleverqa.com