This page is for testers and engineers facing a performance testing round, from a first QA role to a senior performance lead. Most rounds start with test types and metrics like percentiles and throughput, then move to workload modelling, think time and hands-on JMeter: thread groups, correlation and test data. Senior rounds add finding bottlenecks, reading results honestly, realistic environments, writing requirements and reporting to managers, plus how JMeter compares with k6 and Gatling. Each question shows what the interviewer is really checking, the shape of a strong answer and a short answer you can say out loud. Practise saying them, then swap in your own projects.
Search all questions by round, difficulty and level, or save the ones you want to practise.
Load: expected peak traffic for a set time; checks you meet the agreed targets.
Stress: push past peak until something breaks; finds the limit, how it fails and whether it recovers.
Spike: a sudden jump in users; checks autoscaling, queues and recovery.
Soak: normal load held for hours; finds leaks and slow drift.
"They all put load on the system, but each one answers a different question. A load test runs the traffic we expect at peak, like the busiest hour, and checks we meet the response time and error targets. A stress test keeps adding load past that point until something gives, so we learn where the limit is, what fails first, and whether the system recovers cleanly when the load drops. A spike test throws a sudden burst at it, like a sale opening or a push notification going out, to see if autoscaling and queues cope. A soak test holds normal load for many hours, sometimes overnight, to catch memory leaks, growing queues or connection pools that slowly run dry. Before a big launch I'd usually run the load test first, then stress and spike, and a soak before the release goes out."
Describing all four as the same test with a bigger user count, with no idea what question each one answers.
Memory: heap used after each garbage collection; a rising floor points to a leak.
Pools and handles: database connections, threads or file handles that are taken and never returned.
Growing data: tables, caches, queues or logs that grow during the run and slow queries or disks.
Proof: match the drift to the one resource that grows in step with it.
"A slow climb over hours is the classic soak finding, so first I'd line up the response time graph with server metrics over the same window. On a JVM or a similar runtime, I'd look at memory after each garbage collection. Normal is a sawtooth that drops back to a steady floor; if that floor keeps rising, something is holding on to objects, and a heap dump will show what. Next I'd check pools: database connections, worker threads, open file handles. If something takes one and doesn't give it back, requests start queueing. Then I'd look at data that grows with the run, like a table the test keeps inserting into with no index, a cache with no eviction, or logs filling the disk. The resource that grows in step with the slowdown is usually the answer, and I'd give developers that evidence, not just the graph."
Blaming the network, or restarting the servers and re-running, without looking at what grew over time.
Meaning: p95 is the time that 95 out of every 100 requests met or beat.
Averages hide tails: a few very slow requests barely move the mean.
Report the shape: median, p90 or p95, p99 and max together.
"The average hides the people who had a bad time. If most requests take 200 milliseconds and a handful take ten seconds, the average can still look fine, but those users are the ones who complain or leave. A percentile tells you how bad it gets for a given share of users. The 95th percentile is the time that 95 out of every 100 requests came in at or under, so only 5 in 100 were slower. That's why most targets are written as percentiles, like p95 under two seconds. When I report, I show the median for the typical experience, p90 or p95 for the target, p99 for the tail, and the max, and I look at the gaps between them. A big gap between the median and p99 tells me something is slow now and then, and that's worth digging into."
Defining p95 as some fraction of the average, or saying the average is enough as long as the test passes.
Little's Law: users in the system = throughput × time each user spends per cycle.
With think time: users = throughput × (response time + think time).
Use it twice: to size the test before, and to check the results after.
"They're tied together by Little's Law. The number of users in the system equals the throughput multiplied by the time each one spends per cycle, and in a load test that cycle is response time plus think time. So if I have 100 users, each request takes one second and they think for four seconds, the throughput works out at 100 divided by five, which is 20 requests a second. It's useful in two ways. Before the test, it tells me how many virtual users I need for a target rate. After the test, it's a sanity check: with a fixed number of users, if response times go up, throughput has to fall. If my results show throughput unchanged while response time doubled, something in the test or the numbers is off, and I'd check that before reporting anything."
Thinking throughput can be raised without limit just by adding users, or not knowing the three numbers must agree.
Default: tools count connection failures and HTTP 4xx or 5xx codes as errors.
Hidden failures: a 200 page that says something went wrong, a login page after a broken session, an empty result.
Fix: assertions on status and content for every key request, and errors reported by type and time.
"Error rate is failed requests over total requests, usually shown per transaction and over time. The catch is what counts as failed. By default JMeter marks a sample as failed on connection errors and 4xx or 5xx status codes. But plenty of apps return HTTP 200 with an error page, or quietly send you back to the login page when the session broke, and the tool counts that as a pass. Those fake passes are often very fast, so they make response times look better too. So on every key request I add assertions: check the status code, look for text that only appears on success, like an order number, and sometimes check the response size. When I report, I break errors down by type and by when they started, because five errors spread across an hour and five hundred in the last ten minutes are very different stories."
Trusting a zero error rate without any assertions on what the responses actually contain.
Connect time: time to open the connection, including the TLS handshake on HTTPS.
Latency: from just before sending the request to the first part of the response; it includes connect time.
Elapsed: from just before sending to the last byte received; the full response time for that sampler.
"JMeter records three timings for each sample. Connect time is how long it took to set up the connection, including the TLS handshake on HTTPS. Latency runs from just before the request is sent until the first part of the response comes back, so it includes the connect time plus the time the server spent before it started replying. Elapsed time runs until the last byte arrives, so it adds the time to download the whole body. If connect time is small but latency is high, the server is slow to start answering. If latency is fine but elapsed is big, it's a large payload or a slow link. One thing I always point out is that JMeter doesn't render pages or run JavaScript, so elapsed is the time for that request as seen from the load machine, not the page load a user sees in a browser."
Treating JMeter's elapsed time as the full page load time a user sees in the browser.
Threads: each thread is one virtual user running the thread group's samplers in order.
Ramp-up: seconds to start all threads; 100 threads over 50 seconds starts one every half second.
Loop count or duration: how many times each thread repeats, or how long the test runs.
"Each thread is one virtual user that runs through the samplers in the thread group in order, so the number of threads is how many users are active at full load. Ramp-up period is how many seconds JMeter takes to start them all, spread evenly, so 100 threads with a 50 second ramp-up starts a new user every half second. I always use a ramp-up, because starting everyone at once is a spike test by accident, and a gradual ramp shows me where response times start to bend as load grows. Loop count says how many times each user repeats the journey. For real load tests I usually set it to infinite and use the thread lifetime duration instead, so the test runs for a fixed time, like an hour at steady load after the ramp."
Saying ramp-up is how long each user waits between requests, or using zero ramp-up for a normal load test.
Structure: test plan, thread groups, logic controllers, and samplers that send the requests.
Order around a sampler: config elements, pre-processors, timers, the sampler, post-processors, assertions, listeners.
Scope: an element applies to every sampler at its level and below in the tree.
"At the top is the test plan, which holds one or more thread groups, and each thread group is a set of virtual users. Samplers do the actual work, like an HTTP request or a JDBC query, and logic controllers group them or add loops and conditions. Around each sampler JMeter runs things in a fixed order: config elements like HTTP defaults and the cookie manager, then pre-processors, then timers, then the sampler itself, then post-processors like extractors, then assertions, and finally listeners that collect results. Scope matters too. An element applies to every sampler at its level and below, so a timer placed at thread group level fires before every single sampler, which surprises people. I keep timers and extractors as children of the specific sampler they belong to, so the plan does exactly what it looks like it does."
Not knowing that timers run before samplers, or putting every element at the top level without thinking about scope.
Problem: a recording carries tokens that were only valid for that one session.
Find: record the flow twice and compare, or search requests for long random-looking values.
Extract: add an extractor to the response that first returns the value and save it in a variable.
Reuse and check: send the variable in later requests, with a default value and an assertion.
"Correlation means capturing a value the server generates at run time and sending it back in later requests. A recorded script is full of values like session IDs, CSRF tokens or order IDs that were only valid for that one recording, so if I replay it with fifty users the server rejects them. To find them, I record the flow twice and compare, or search for long random-looking strings in the requests. Then I find the first response that returns the value and add an extractor to that sampler: a JSON Extractor for an API, a Regular Expression or Boundary Extractor for HTML. It saves the value in a variable, and I replace the hard-coded value in later requests with that variable. I always set a default like NOT_FOUND and add an assertion, so if extraction fails the test fails loudly instead of sending garbage."
GET /login
Boundary Extractor (child of this sampler)
Field to check: Body
Name of created variable: csrf
Left Boundary: name="_csrf" value="
Right Boundary: "
Match No.: 1
Default Value: NOT_FOUND
POST /login
Parameters: username=${user} password=${pass} _csrf=${csrf}
Response Assertion
Field to Test: Text Response
Rule: Contains
Pattern: Sign out
Replaying a recorded script with hard-coded tokens and blaming the server when every user after the first one fails.
CSV Data Set Config: reads the next line of a file into variables on each thread iteration.
Sharing mode: by default all threads share one file pointer, so each line goes to one user.
End of file: recycle when data can be reused; stop the thread when each line is single use.
Enough rows: size the file for users × iterations when data can't be reused.
"I use a CSV Data Set Config. I put the test data in a file, like username, password and customer ID per line, and name the variables in the config. Each time a thread starts a new iteration it takes the next line, and I reference those variables in the requests. The settings that matter are sharing mode, recycle on end of file, and stop thread on end of file. With the default sharing across all threads, each read takes the next line, so users get different rows, and with plenty more rows than active users, two users almost never hold the same account at once. If the data can be reused, I let it recycle. If each line can only be used once, like a one-time voucher, I turn recycling off, stop the thread at end of file, and make sure the file has enough rows. One account for everyone mostly tests the cache and session locking, not the real system."
Running every virtual user with the same login and treating the result as realistic.
GUI cost: the GUI and live listeners use a lot of memory and CPU on the load machine.
Non-GUI: run from the command line, write raw results to a file, build the HTML report at the end.
Properties: pass users and duration on the command line so one plan serves every size of run.
"The GUI is for building and debugging the script with a handful of users. At real load, the GUI and listeners like View Results Tree hold responses in memory and eat CPU, so the load machine itself becomes the bottleneck and the timings get skewed. So I run from the command line in non-GUI mode. I pass the test plan and a results file, and ask JMeter to build the HTML dashboard at the end into an empty folder. I also pass the thread count, ramp-up and duration as properties, so the same plan can run a small smoke test or a full load test without editing it. And I disable heavy listeners in the plan, because in non-GUI mode the results file is all I need, and I can rebuild the report from it later."
jmeter -n -t checkout.jmx -l results.jtl -e -o report/ \
-Jusers=200 -Jrampup=300 -Jduration=3600
# in the thread group, read them with ${__P(users,10)} and so on
Running a full load test from the GUI with View Results Tree switched on.
Setup: one controller sends the plan to several worker machines, which run it and send results back.
Thread count: each worker runs the whole plan, so total users = threads in the plan × workers.
Traps: CSV files must exist on every worker, versions must match, and firewalls must allow the RMI ports.
Watch the workers: CPU, memory and network, so they don't become the bottleneck.
"In distributed mode there's one controller and several worker machines running the JMeter server. The controller sends the test plan to every worker, each one runs it, and results stream back to the controller. The first trap is the maths: each worker runs the whole plan, so if the thread group says 100 threads and I have four workers, that's 400 users, not 100. The second is data. CSV files aren't copied across, so each worker needs its own file at the same path, and if logins must be unique I split the data so workers don't overlap. Then there's the plumbing: the same JMeter and Java versions everywhere, and the RMI ports open through firewalls. Finally I watch the workers' CPU, memory and network during the run, and keep them close to the system under test on the network, so I'm measuring the app and not a long network path."
Setting 100 threads on four workers and reporting it as a 100 user test.
Sources: production access logs, analytics and business forecasts for the peak you care about.
Journeys and mix: the main user paths and what share of traffic each one takes.
Rates: peak hour transactions per journey, concurrency, think time and planned growth.
Data spread: how many distinct users, products or accounts the traffic touches.
"I start with data, not guesses. I'd pull production access logs and analytics for the busiest period, usually the peak hour of the peak day, and if the app is new I'd use business forecasts and similar systems. From that I pick the handful of user journeys that carry most of the load or most of the risk, like browse, search, add to cart and checkout, and work out the mix, say for every ten visitors who browse, three search and one buys. For each journey I note the transactions per hour at peak, the typical think time between steps, and how many users are active at once. I also note the data spread, like how many different products get viewed, because hitting the same item every time only tests the cache. Finally I agree a growth factor with the business, so we test for next year's peak, not last year's."
Picking a round number of users with no link to real or forecast traffic.
Think time: a pause between steps inside a journey, the time a person spends reading or typing.
Pacing: the gap from the start of one iteration to the start of the next, which sets how often each user repeats the journey.
Leaving them out: each virtual user fires requests back to back, so the load per user is far higher than real.
"Think time is the pause between steps inside a journey, like the few seconds someone spends reading a product page before clicking add to cart. Pacing is about the whole iteration: it sets how often each virtual user starts the journey again, so with sixty seconds of pacing that user starts a new journey once a minute, however fast the steps went. Together they let me hit a target transaction rate with a realistic number of users. If I leave them out, each virtual user fires requests back to back, so fifty virtual users can generate the traffic of hundreds of real people. The server sees a pattern no real crowd makes, fewer open sessions but a much higher rate, and the results don't tell you much about the real peak. I also add some randomness to think time so users don't all click in lockstep."
Removing all think time to make the test harder, then reporting the virtual user count as if it were real users.
Closed: a fixed number of users; each waits for a response before sending the next request.
Open: new requests arrive at a set rate whatever the system is doing, like visitors to a public site.
Why it matters: in a closed model a slow system gets fewer requests, so load backs off and slow times are under-reported.
Tools: a classic JMeter thread group is closed; k6 arrival-rate executors and Gatling open injection are open.
"In a closed model I have a fixed pool of users, and each one waits for its response before sending the next request. In an open model requests arrive at a set rate however the system is coping, which is closer to a public website where new visitors keep turning up. The difference shows when the system slows down. In a closed model, slow responses mean each user sends fewer requests, so the load quietly drops just when the system is struggling, and the requests that would have been sent during the stall are never measured. That's often called coordinated omission, and it makes latency look better than real users would see. In an open model the requests keep coming, queues build, and you see the real pain. So for public traffic I prefer an arrival-rate model, and for an internal system with a fixed number of staff, a closed model can be the honest one."
Not knowing the load tool's model affects the results, or claiming both models always give the same numbers.
Rate: 7,200 an hour divided by 3,600 seconds is 2 orders a second.
Little's Law: users = rate × time per iteration = 2 × 90 = 180 virtual users.
Hold the rate: trim think time slightly and use pacing to fix each iteration at 90 seconds.
Verify: compare achieved orders per second with the target during the run.
"First I turn the target into a rate per second: 7,200 orders an hour over 3,600 seconds is two orders a second. Each virtual user takes about 90 seconds per journey, including think time. Little's Law says users equal rate times time in the system, so two times 90 gives 180 virtual users. That's the minimum. Under load the responses may slow down, which stretches each iteration and drops the rate. So I'd trim the think time a little and use pacing to hold each iteration at 90 seconds, which leaves some slack to absorb slower responses, and I might add a few spare users. During the run I'd check the actual orders per second against the target. If we're short and the app is slow, that's a finding. If we're short and the app looks fine, the test needs more users."
Configuring 7,200 users because the target is 7,200 orders, or never checking the achieved rate during the run.
Read the curve: the knee where throughput goes flat is where some resource saturated.
Check each tier: utilisation, saturation and errors for CPU, memory, disk and network on every server.
Check the queues: thread pools, database connection pools, locks, slow queries and downstream calls.
Prove it: change one thing and re-run to see if the knee moves.
"That pattern means something is saturated: work is queueing, so each request waits longer but no more requests finish per second. First I note the load level at the knee. Then I go tier by tier, from the load balancer to the app servers to the database, and for each one I check utilisation, saturation and errors on CPU, memory, disk and network. If CPU is pinned on the app servers, I'd profile the code. If CPU is low everywhere, it's usually a queue: worker threads all busy, the database connection pool empty with requests waiting for a connection, lock contention, or a slow downstream service. APM traces help a lot here, because they show where the time inside each request actually goes. Then I prove it with one change, like a bigger pool or a missing index, and re-run. If the knee moves up, we found it, and often the next bottleneck appears right behind it."
Saying 'add more servers' without identifying which resource actually saturated.
Read the pattern: regular spikes point to something scheduled or cyclic, not to the load itself.
Usual causes: garbage collection pauses, mass cache expiry, scheduled jobs, log rotation, autoscaling, connection recycling.
Correlate: plot raw response times over time next to GC logs, job schedules and infrastructure events.
"A steady median with regular p99 spikes tells me most requests are fine and something periodic hurts a few of them. So I'd plot the raw response times over time, measure the gap between spikes, and look for anything on that same cycle. The usual suspects are garbage collection, especially long collections that pause the app; cache entries that all expire at once, so a wave of requests hits the database together; scheduled jobs like reports or backups running against the same database; log rotation or disk flushes; connection pools recycling connections on a timer; and autoscaling adding or removing instances. I'd line up GC logs, job schedules and infrastructure events against the spikes. Once the timing matches one of them, the fix is usually clear, like tuning the collector, staggering cache expiry, or moving the job away from peak hours."
Ignoring the spikes because the median and the average both pass the target.
Phases: ramp-up, steady state, ramp-down; the steady state answers the load question.
Warm-up: cold caches, opening pools and code still being compiled make the first minutes slow.
Be open: state the window used, and still read the ramp-up for where things began to bend.
"A run has phases: ramp-up, a steady state at target load, and ramp-down. The question 'do we meet the target at peak load' is answered by the steady state, so that's the window I use for the headline numbers. The first minutes are usually slower because caches are cold, connection pools are still opening and, on some runtimes, code is still being compiled, so including them makes results look worse than a warmed-up production system. Ramp-down numbers mislead too, because load is falling. But I don't throw the rest away. The ramp-up is where I see response time start to bend as users increase, which hints at where the limit is. And in the report I say exactly which window I used, so nobody thinks I picked the best-looking ten minutes."
Averaging the whole run including ramp-up and ramp-down, or quietly picking the best-looking slice.
Verdict first: pass or fail against each agreed requirement, in a short summary.
A few charts: response time and throughput as users grow, with the target line, compared with the last release.
Risks and actions: bottlenecks, environment differences, fixes and owners; raw detail in an appendix.
"I'd lead with the answer, not the charts. The first few lines say plainly whether we met each agreed target, something like: checkout meets its target at the planned peak, but search slows past its target once we reach about four fifths of peak. Then I'd show a small number of charts that tell the story, like response time and throughput as users increase, with the target line drawn on, and how this build compares with the last release. After that come the risks in plain words: what the bottleneck is, what it means for users, and how the test environment differs from production. I finish with recommendations and owners, like adding an index before release and retesting after the fix. The detailed tables and raw data go in an appendix for the engineers, so the main report can be read in a couple of minutes."
Sending a raw tool dashboard with dozens of graphs and no clear verdict.
Match what matters: same versions and config, similar hardware per node, same topology with load balancer and cache.
Data volume: a production-sized, anonymised database, since queries on small tables are always fast.
Dependencies: third parties stubbed with realistic delays, and no other traffic on the environment.
State the gaps: list every difference and how it could change the result.
"The closer the environment is to production, the more the numbers mean. What I care about most is the same software versions and configuration, like pool sizes and memory settings, similar hardware per server, and the same shape, with the load balancer, cache and database set up the same way. Data volume is huge: a query that's instant on ten thousand rows can crawl on fifty million, so I want a production-sized, anonymised database. For third parties like a payment gateway I use stubs, but I give them realistic response times, because a stub that answers instantly hides real waiting. I also make sure nobody else is running tests on the environment during my run. If it's smaller than production, say half the servers, I don't just double the result. I measure capacity per server and write every difference and its likely effect in the report."
Testing against a tiny database or a busy shared environment and presenting the numbers as production capacity.
Per transaction: targets for the key journeys, not one number for the whole app.
Full statement: percentile, time, load level, error limit and duration.
Sources: production data, forecasts, user expectations and today's baseline.
Agree it: signed off before testing, so pass or fail isn't argued afterwards.
"'Fast' can't pass or fail, so I'd sit down with the stakeholder and make it measurable. First, which transactions matter: login, search and checkout usually have different expectations, and a report export can take longer. Then for each one I write a full statement: the percentile, the time, the load level, the error limit and how long it has to hold. For example, checkout p95 under two seconds at 50 orders a second, with fewer than one error in a thousand requests, held for an hour. The load level comes from production data or forecasts, and the time targets from what users tolerate and what the system does today, so we don't get worse. I also ask about resource headroom, like CPU staying at a comfortable level at peak. Then we agree it in writing before testing, so a result is a clear pass or fail, not a debate."
Accepting 'fast' or 'under two seconds' with no percentile, load level or error limit attached.
JMeter: mature, GUI to build plans, wide protocol support and plugins; a thread per user, so heavier per user.
k6: tests written in JavaScript, run from the command line, thresholds that pass or fail a CI job.
Gatling: tests written as code in a DSL, efficient non-blocking engine, detailed HTML reports.
Choose by: team skills, protocols, CI needs, scale and what's already in use.
"JMeter is the most established. It has a GUI for building plans, supports protocols beyond HTTP like JDBC and JMS, and has a large plugin ecosystem. The trade-offs are that plans are XML files that are awkward to review, and each virtual user is a thread, so it needs more memory per user. k6 is code first: tests are JavaScript, it runs from the command line, and thresholds decide pass or fail, which fits nicely into a CI pipeline. Gatling is also code first, with a DSL, and its non-blocking engine can drive a lot of users from one machine, with good HTML reports. I'd pick JMeter when testers want a GUI or we need less common protocols, and k6 or Gatling when developers own the tests and we want them in the pipeline. Honestly, what the team already knows often decides it."
Calling one tool the best for everything, or choosing without mentioning the team or the protocols.
Options: stages set the ramp; thresholds set pass or fail on metrics.
Default function: each virtual user repeats it: request, check, then sleep as think time.
Result: a crossed threshold makes k6 exit with a non-zero code, which fails the CI job.
"In k6 the load profile and the pass rules live in the exported options. I use stages to ramp up to 50 users over a minute, hold for five minutes, then ramp down. In thresholds I say the 95th percentile of request duration must stay under 500 milliseconds and failed requests must stay under one in a hundred. The default function is what each virtual user repeats: it makes the request, uses check to confirm the status is 200 and the body has what I expect, then sleeps a second as think time. Checks on their own don't fail the run, they only record a pass rate, so the thresholds are what make it fail. If any threshold is crossed, k6 ends with a non-zero exit code, so the pipeline step fails without anyone having to read a graph."
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '1m', target: 50 },
{ duration: '5m', target: 50 },
{ duration: '1m', target: 0 },
],
thresholds: {
http_req_duration: ['p(95)<500'],
http_req_failed: ['rate<0.01'],
},
};
export default function () {
const res = http.get('https://test.example.com/products');
check(res, {
'status is 200': (r) => r.status === 200,
'lists products': (r) => r.body.includes('product'),
});
sleep(1);
}
Relying on checks alone to fail the run, or leaving out think time so 50 users hammer the endpoint nonstop.
Context: the system, why it needed testing, and the targets you agreed.
Design: journeys, workload mix, test data, environment and tool.
Run and analyse: what you ran, what you found, how you proved it.
Outcome: the decision it drove and what you'd do differently.
"At my last company we were launching a new order-tracking API before a seasonal peak. The business expected about three times normal traffic, so I agreed targets with the product owner: p95 under one second for the two main calls at that load, with fewer than one error in a thousand. I pulled a week of access logs to get the mix, mostly status lookups with some history queries, and built the JMeter plan with a CSV of real, anonymised order numbers so we weren't just hitting the cache. We ran a load test at peak, then a stress run past it, all in non-GUI mode. The history call missed its target at peak, and traces showed a query scanning a large table. The team added an index, I re-ran, and it passed comfortably. If I did it again, I'd get production-sized data in place earlier."
A story that only covers recording and running a script, with no targets, no workload reasoning and no outcome.
Symptom: what the results showed and at what load.
Investigation: the metrics, dumps or traces that narrowed it down.
Proof: the change and the re-test that confirmed it.
Result: the impact, and what the team kept doing afterwards.
"On a booking system I tested, search response times were fine up to about 150 users, then climbed steeply while throughput went flat. The odd part was that CPU on the app servers was only about half used and the database looked quiet. I took thread dumps from the app during the run, and most worker threads were waiting for a database connection. The pool was set to 20 connections, copied from a template, and each search ran two queries. So the database was fine, it just wasn't being asked. I shared the thread dumps and pool metrics with the developers, we raised the pool size to what the database could safely handle, and I re-ran. The knee moved out past 400 users, and the next limit was real database CPU. After that we added pool usage to the standard monitoring dashboard."
Claiming a fix with no evidence, or telling a story where someone else found the cause and you only re-ran the test.
What happened: the production problem and how it differed from the test.
Why the test missed it: workload, data, environment or a scenario that wasn't modelled.
Change: what you added so the same gap can't slip through again.
"We once passed a load test for a reporting feature, and then at the first month-end in production it slowed to a crawl. When I dug in, the gap was in my workload model. I'd built it from an average week of logs, and month-end looked completely different: finance users ran big exports at the same time, and each one held database locks that blocked normal users. None of that was in my test. I owned it in the review and changed three things. We started building the workload model from the peak business period, not a typical week, and asked the business about calendar events up front. We added the export journey as background load running alongside the normal mix. And we began comparing test assumptions with production monitoring after each release. The next month-end went through without trouble."
Blaming production, the users or the developers without admitting what the test missed.
Ask the minimum: the decision it feeds, the key journeys, the expected peak, what good looks like.
Scope to fit: a focused test on the riskiest journeys that can be done properly in time.
State the limits: assumptions written down, and what a quick test cannot tell them.
"I wouldn't refuse, and I wouldn't just point a tool at the homepage either. I'd ask the manager for fifteen minutes to find out what decision the test feeds, like whether a release can go out or whether we can handle a marketing campaign. Then I'd pin down the minimum: the two or three journeys that matter most, the expected peak, taken from logs if they exist, and a rough idea of acceptable response times. If they can't give targets, I'd propose some based on how production behaves today and get a quick yes. Then I'd scope a test I can do properly by Friday, maybe one run at expected peak and a short step-up to see where it bends. In the report I'd list every assumption clearly, so nobody reads a quick check as full sign-off."
Running a random number of users against one page and reporting that it passed, with no targets behind it.
Check it's real: repeatable across runs, and not caused by the environment or load generator.
Size the risk: how far over, from what load, and how often production reaches that load.
Recommend, don't hide: options with their risks, decided by the release owner and written down.
"I wouldn't quietly pass it, and I wouldn't block the release on my own either. First I'd check the result is real: was it consistent across runs, and was anything odd in the environment or the load generator during that test? If it's genuine, I'd size the risk. How far over the target is it, at what load does it cross the line, and how often does production actually reach that peak? Just over target at the very top of forecast peak is a different risk from failing at normal traffic. Then I'd take that to the release owner with options, like releasing with close monitoring and a rollback plan, releasing with a known fix to follow, or delaying. My job is to make the risk clear and honest. The decision belongs to the people accountable for the release, and I'd make sure it's written down."
Rerunning until one run passes, or quietly loosening the target, and then signing off.
Take it seriously: an overloaded load generator really does distort results.
Check the generator: CPU, memory, network and garbage collection on the load machines.
Compare sources: server-side timings from access logs or APM against the tool's timings.
Test it: spread the same load over more machines and see if the results change.
"I'd take it seriously, because they could be right. A load machine that's out of CPU or memory, or pausing for long garbage collections, adds its own delay to every measurement. So I'd share the load generator's own metrics from the run: CPU, memory, network and, for JMeter, its heap and GC activity. Then I'd compare the tool's timings with the server's own view, from access logs or APM traces, which record how long the app took to handle each request. If the server-side times are also high, the slowness is in the app. If the server says it answered in 100 milliseconds and the tool says two seconds, the problem is somewhere between them, the generator or the network. To settle it, I'd re-run with the load spread over more machines. If the numbers don't change, it isn't the tool. And I'd invite the developer to look at the data with me."
Getting defensive and insisting the tool is right without showing any load generator or server-side evidence.
ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your resume and notes are never stored on our servers. It stays out of screen share on every plan; only you can see it.