The most surprising thing about JMeter’s throughput and error metrics is that they often tell you less about your application’s performance than you think, and more about your load generator’s limitations.

Let’s see JMeter in action. Imagine we’re testing a simple API endpoint that returns a list of users.

Here’s a basic JMeter test plan structure:

<hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="User API Test" enabled="true">
        <stringProp name="TestPlan.comments"></stringProp>
        <boolProp name="TestPlan.functional_mode">false</boolProp>
        <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
        <elementProp name="TestPlan.userDefinedVariables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" enabled="true">
            <collectionProp name="Arguments.arguments"/>
        </elementProp>
        <stringProp name="TestPlan.userDefinedStrings"></stringProp>
    </TestPlan>
    <hashTree>
        <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Users Thread Group" enabled="true">
            <stringProp name="ThreadGroup.on_thread_stop">donothing</stringProp>
            <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" enabled="true">
                <boolProp name="LoopController.continue_forever">true</boolProp>
                <stringProp name="LoopController.loops">-1</stringProp>
            </elementProp>
            <stringProp name="ThreadGroup.num_threads">100</stringProp>
            <stringProp name="ThreadGroup.ramp_time">10</stringProp>
            <longProp name="ThreadGroup.start_time">1678886400000</longProp>
            <longProp name="ThreadGroup.end_time">1678886400000</longProp>
            <boolProp name="ThreadGroup.scheduler">false</boolProp>
            <stringProp name="ThreadGroup.duration"></stringProp>
            <stringProp name="ThreadGroup.delay"></stringProp>
            <boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
        </ThreadGroup>
        <hashTree>
            <HTTPSamplerProxy guiclass="HttpSamplerGui" testclass="HTTPSamplerProxy" testname="GET /users" enabled="true">
                <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
                    <collectionProp name="Arguments.arguments"/>
                </elementProp>
                <stringProp name="HTTPSamplerProxy.domain">your-api-domain.com</stringProp>
                <stringProp name="HTTPSamplerProxy.port">8080</stringProp>
                <stringProp name="HTTPSamplerProxy.protocol">http</stringProp>
                <stringProp name="HTTPSamplerProxy.contentEncoding"></stringProp>
                <stringProp name="HTTPSamplerProxy.path">/users</stringProp>
                <stringProp name="HTTPSamplerProxy.method">GET</stringProp>
                <boolProp name="HTTPSamplerProxy.follow_redirects">true</boolProp>
                <boolProp name="HTTPSamplerProxy.use_keep_alive">true</boolProp>
                <boolProp name="HTTPSamplerProxy.do_ூ_auth">false</boolProp>
                <stringProp name="HTTPSamplerProxy.authentication">basic</stringProp>
                <stringProp name="HTTPSamplerProxy.username"></stringProp>
                <stringProp name="HTTPSamplerProxy.password"></stringProp>
                <boolProp name="HTTPSamplerProxy.embedded_resources">false</boolProp>
                <stringProp name="HTTPSamplerProxy.connect_timeout"></stringProp>
                <stringProp name="HTTPSamplerProxy.response_timeout"></stringProp>
            </HTTPSamplerProxy>
            <hashTree/>
        </hashTree>
    </hashTree>
</hashTree>

When we run this with 100 threads, we’ll see results in JMeter’s "View Results Tree" or, more practically, in a summary report.

The Throughput metric, typically measured in requests per second (RPS), tells you how many requests your JMeter client successfully sent and received a response for within a given time frame. High throughput seems good, but if your JMeter machine is maxed out on CPU or network, it’s not your application that’s performing well, it’s just that your load generator can’t send any more load.

Errors are crucial. A 0% error rate is the ideal, but what constitutes an "error" in JMeter? By default, it’s network-level errors (connection refused, timeouts) or HTTP status codes above 400. However, you can configure JMeter to treat specific HTTP status codes (like 500s or even 429s) as errors using Assertions.

Latency is the time from when JMeter sends a request to when it receives the first byte of the response. This is different from the Response Time, which is the total time from sending the request to receiving the entire response. High latency often points to network issues or initial processing delays on the server.

To truly understand your application, you need to look beyond JMeter’s default output. The core problem JMeter solves is simulating user load to identify bottlenecks in your application, not necessarily in your testing infrastructure.

Consider the Average Response Time. If your average response time is steadily increasing with load, and your throughput is plateauing, that’s a strong indicator that your application is struggling to keep up. This is often where the "surprising" part comes in: you might see a high RPS, but if the average response time is also high, it means each individual request is taking a long time, and your application is likely saturating a resource (CPU, memory, database connections).

The most common pitfall is interpreting a high throughput value from a single JMeter instance as proof of application scalability. If you run the same test from multiple JMeter machines (distributed testing) and the total throughput across all machines increases proportionally, then you’re likely seeing your application’s true capacity. If the total throughput remains the same or increases only slightly, your bottleneck is probably the application itself, not the load generation.

What most people miss is the impact of JMeter’s own resource consumption. When JMeter generates a lot of load, it consumes CPU, memory, and network bandwidth on the load generator machine. If these resources become saturated, JMeter cannot send requests fast enough or receive responses promptly, leading to artificially low throughput and increased response times that don’t reflect the application’s actual performance. This is why using distributed testing or dedicated load generators is critical.

The next logical step after understanding these metrics is to correlate them with server-side metrics using tools like Prometheus or Datadog to pinpoint the exact resource (CPU, memory, disk I/O, database locks) causing the observed performance degradation.

Want structured learning?

Take the full Jmeter course →