In a 500-thread JMeter plan, adding a Response Assertion to every sampler left response times flat but cut throughput. Why?
answer
- sampleEnd runs before the assertion does
- No JTL column records assertion time
- Cost lands between samples, not inside one
- Compare against the same plan with it disabled
basics
~20 sThe sampler stamps its own end time before the assertion runs, so the check sits outside the reported duration. It is still inside the thread's loop, so each thread gets through fewer samples per second.
solid answer
~50 sA JMeter sample's duration is fixed by `SampleResult.sampleEnd()`, called inside the sampler: `elapsedTime` is computed there as end minus start minus idle time. `checkAssertions(...)` is called afterwards, from `JMeterThread`, so no assertion CPU is ever added to that sample's `elapsed` or `Latency` figure. The cost is real all the same. It is spent on the virtual user's own thread, between one sample and the next, so the thread starts its following request later; with 500 threads doing it at once the injector's cores are shared between making requests and checking them. The symptom is exactly the pair you saw — per-sample times unchanged, samples per second down. To confirm the assertion is the cause rather than the system under test, disable it and rerun the same plan, and watch injector CPU rather than the response-time columns.
code
text · 3 linestimeStamp,elapsed,label,responseCode,success,Latency,Connect
1757376000123,214,GET /catalog,200,true,198,12
1757376000341,209,GET /catalog,200,true,193,11go deeper
Remember the shape of the answer even if you cannot yet diagnose it: the check happens after the sampler has finished timing itself, so it costs the generator without showing in the response time.
Explain the mechanism precisely — sampleEnd fixes elapsed inside the sampler, checkAssertions runs after it — and name which columns therefore cannot move.
Drive the diagnosis: one variable changed per run, injector CPU rather than target metrics, and a count of evaluations that accounts for samplers, threads, iterations and sub-samples.
Make the invisibility a standing risk you manage: agree what a load plan is allowed to verify inline, and require an injector-CPU headroom check before a plan is trusted to push a target number.
## Why the two numbers move independently The timing of a JMeter sample is closed before the assertion is ever consulted: 1. The sampler calls `sampleStart()`, issues the request and waits. 2. When the reply is complete it calls `sampleEnd()`, which sets `endTime` and computes `elapsedTime = endTime - startTime - idleTime`. 3. The sampler returns its `SampleResult` to `JMeterThread`. 4. `JMeterThread` runs post-processors, then `checkAssertions(...)`, then notifies listeners. Steps 3 onward cannot change `elapsedTime`; it is already a number. So an assertion can burn milliseconds of CPU per sample and every response-time column in the run — `elapsed`, `Latency`, and everything the report derives from them — will be untouched. What *does* change is how quickly the thread comes back round. A JMeter virtual user is a platform thread running a loop, so any work it does between samples pushes the next request later. Fewer requests per thread per second, across 500 threads, is a visible drop in samples per second. ## The measurement trap this creates This is the most useful thing to say in an interview, because it is counter-intuitive: **the JTL has no column for what an assertion cost.** A row looks the same with or without the check. ``` timeStamp,elapsed,label,responseCode,success,Latency,Connect 1757376000123,214,GET /catalog,200,true,198,12 ``` The consequence is that the usual instinct — "response times are fine, so the generator is fine" — is exactly wrong here. Response times are the one signal that is guaranteed *not* to move. ## How to confirm it is the assertion A short, ordered diagnosis: 1. **Rerun the identical plan with the assertion disabled.** Same threads, same ramp, same duration, same target. If samples per second recovers and response times are unchanged, you have your answer with one variable moved. 2. **Watch CPU on the injector, not on the target.** Assertion cost is generator-side; if the injector is pinned while the system under test is idle, the work is being done in the wrong place. 3. **Count evaluations, not elements.** Samplers times threads times iterations, multiplied again if *Apply to* is set to include sub-samples, which descends four levels into a result's children. 4. **Look at what the assertion is fed.** A check on `Response Code` reads three characters; a check on `Text Response` reads the whole body, and a check on `Document (text)` runs an Apache Tika parse of it. 5. **Look at how many patterns each assertion carries.** Patterns are tested in order and, in AND mode, evaluation stops at the first failure — so on a *passing* run every pattern is evaluated, every time. ## The wider point The reason this question is asked at senior level is that it separates people who read dashboards from people who understand where the run's time goes. Assertion cost is a load-generator capacity item that hides behind a correctness feature. It never appears in the metric everyone watches, and it only shows up as "we could not push as hard as we expected" — which is very easy to blame on the system under test. How much verification a run should do at all is a workload-design question and not a JMeter one. What JMeter tells you is precisely where the CPU goes when you have decided: on the sampler thread, between the reply and the next request, invisible to every timing column in the results file. ## What does not explain it - **"The assertion made the server slower."** Assertions never touch the server; they read a `SampleResult` that is already in memory. - **"The extra failures dragged throughput down."** Failing an assertion marks the sample unsuccessful but does not, by itself, slow anything — unless the sampler's error action is set to stop the thread or the test. - **"Listeners got busier."** The listener sees one extra `AssertionResult` on the sample; that is a write-volume matter, not the CPU that went missing here.
- If the assertion time is not in elapsed, is there anywhere in the results file it can be inferred?Only indirectly. With the shipped `sampleresult.timestamp.start=true`, each row's `timeStamp` is that sample's start, so for one thread the gap between consecutive starts minus the earlier `elapsed` covers assertion, post-processor, listener and timer time together. It is a lumped figure, not an assertion measurement.
- Would moving the assertion onto a Transaction Controller instead of each sampler reduce the cost?It can, because the assertion is then evaluated once against the transaction's result rather than once per child sampler — but only if its Apply to stays on the main sample. Set it to include sub-samples and JMeter walks the children anyway, and you are back to the original count.
- The team wants proof before removing the assertion. What is the cheapest experiment?Run the same plan twice against the same target, once with the assertion enabled and once disabled, holding threads, ramp and duration constant. Compare samples per second and injector CPU. Response times are expected to be identical in both runs, which is itself the confirming signal.
A checkout assistant who scans your shopping at normal speed, then stops to re-read the receipt before calling the next customer. Every individual transaction is timed the same; the queue still moves more slowly.
saying these in an interview costs you the question
- Assumes the assertion inflated the reported response times
- Blames the system under test without checking injector CPU
- Thinks failing assertions are what slowed the run
- Looks for an assertion-duration column in the JTL
- Changes threads and assertions in the same comparison run