Execute and Analyze Results
The script is debugged, the load model is defined - now it's time to run the actual load test and make sense of the results. Always run load tests in CLI mode (command line), never from the GUI.
Why CLI Mode
The JMeter GUI is for building and debugging scripts only. For actual load tests:
| Mode | Purpose |
|---|---|
| GUI | Script development, debugging with 1-3 threads |
| CLI | Running load tests with real thread counts |
Why not GUI for load tests?
- GUI rendering (View Results Tree, graphs) consumes significant CPU and memory
- This overhead distorts the results - response times appear higher than they actually are
- The JMeter process can run out of memory and crash mid-test
- CLI mode uses minimal resources, giving you accurate results
CLI Command
Basic Command
jmeter -n -t test-plan.jmx -l results.jtl -e -o report-folder
| Flag | What it Does |
|---|---|
-n | Non-GUI (CLI) mode |
-t | Path to the .jmx test plan |
-l | Path to write the results file (.jtl or .csv) |
-e | Generate HTML report after the test |
-o | Output folder for the HTML report (must not exist or be empty) |
Example
jmeter -n -t scripts/login-flow.jmx -l results/login-test-run1.jtl -e -o results/report-run1
Useful Additional Flags
| Flag | What it Does |
|---|---|
-Jthreads=100 | Override a JMeter property (if your script uses ${__P(threads,10)}) |
-Jduration=1800 | Override test duration via property |
-Jrampup=300 | Override ramp-up via property |
Using properties (${__P(threads,10)}) in your Thread Group instead of hardcoded values lets you change the load model from the command line without editing the .jmx file. The second value (e.g., 10) is the default if no property is passed.
Capturing Variables in the Results
By default the .jtl tells you that a request failed, not who it failed for. Every row has a label, a response code, and a thread name - but nothing about the data that thread was using. When 40 of 5,000 logins fail, you are left guessing which accounts were involved.
The sample_variables property fixes this. It writes the value of any JMeter variable into every row of the results file:
# In user.properties (JMeter bin/ folder)
sample_variables=username,orderId,customerType
Or pass it at run time without editing any file:
jmeter -n -t test.jmx -l results.jtl -Jsample_variables=username,orderId
Each name becomes one extra column appended to the end of every CSV row (in XML output they become extra attributes instead). So a failing row now tells you the label, the error, and that it was user0417 placing order SO-99821.
Important: This adds a column, not extra rows. Naming a Transaction Controller after the user does the same job by adding a row per user, which then has to be filtered back out before reporting. A column costs you nothing at report time - see Filtering the .jtl below.
Things worth knowing:
- The variable is read at sample time. If it is not set yet when the request fires, the column is empty for that row. Variables from CSV Data Set Config and extractors behave as you would expect; anything set after the sampler will not appear.
- Column names appear in the header when
jmeter.save.saveservice.print_field_names=true(the default), so the file stays self-describing. - Keep the list short. Every name adds a column to every row, and a long test writes millions of rows. Capture what you need to diagnose a failure, not the whole variable space.
- It works in distributed mode. Set the property on the controller - JMeter "sends the variable to all servers to ensure that the correct data is available at the client" (Listeners). See Section 12 and Section 13.
What You See During Execution
While the test runs, JMeter prints a summary line every 30 seconds:
summary + 1250 in 00:00:30 = 41.7/s Avg: 245 Min: 12 Max: 1823 Err: 0 (0.00%) Active: 100
summary = 5000 in 00:02:00 = 41.7/s Avg: 240 Min: 12 Max: 2105 Err: 3 (0.06%) Active: 200
| Field | Meaning |
|---|---|
+ line | Stats for the last 30-second interval |
= line | Cumulative stats since the start |
41.7/s | Throughput (requests per second) |
Avg: 245 | Average response time (ms) |
Min / Max | Minimum and maximum response time (ms) |
Err: 0 (0.00%) | Error count and percentage |
Active: 100 | Currently active threads |
This gives you a live view of how the test is progressing. Watch for errors climbing or response times spiking.
Key Metrics
These are the metrics that matter when analyzing results:
Response Time
| Metric | What it Tells You |
|---|---|
| Average | Overall average response time - can be misleading if there are outliers |
| Median (50th percentile) | Half the requests were faster than this |
| 90th Percentile | 90% of requests were faster than this - the most commonly reported metric |
| 95th / 99th Percentile | Used for stricter SLAs |
| Min / Max | The fastest and slowest individual requests |
Important: Always report percentiles, not just averages. An average of 500ms could mean all requests took ~500ms, or it could mean 99% took 200ms and 1% took 30 seconds. The percentile tells the real story.
Throughput
Requests per second (or transactions per second if using Transaction Controllers). This tells you how much work the system is processing.
- Throughput should stabilize during steady state
- If throughput drops while users are constant, the server is struggling
- Compare actual throughput against the NFR target
Error Rate
The percentage of requests that failed. Failures include:
- HTTP 4xx/5xx responses
- Connection errors / timeouts
- Assertion failures (if assertions are in the script)
The NFR typically defines the acceptable error rate (e.g., < 1%).
Working with the .jtl
The .jtl is the raw output of the run and the input to everything else - the HTML report, the GUI listeners, and any custom analysis you do. Get it into shape before you generate anything from it.
The Results File (.jtl)
The .jtl file contains raw test data - one line per request. You can open it in JMeter's GUI listeners (Aggregate Report, Summary Report) for quick analysis, or use it to regenerate the HTML report.
To open in JMeter GUI:
- Open JMeter (GUI mode)
- Add a listener (e.g., Aggregate Report, Summary Report)
- Click Browse and select the
.jtlfile
To regenerate the HTML report from a .jtl file:
jmeter -g results.jtl -o report-folder
Filtering the .jtl Before Generating Reports
The raw .jtl file contains rows you don't want in the web report. Always filter before generating reports — this both cleans the data and drastically reduces file size, which matters for report generation speed and memory usage.
What gets filtered:
| Filter | What it Removes | Why |
|---|---|---|
| Sub-results | Labels ending with -0, -1, etc. | Transaction Controllers with "Generate parent sample" unchecked produce child samples (e.g., A - Login-0, A - Login-1). These are duplicate data — the parent TC already captures the aggregate. Sub-results typically make up ~80-85% of a raw JTL file. |
| Unresolved variables | Labels containing ${...} | Rows where JMeter variables weren't resolved (e.g., ${username}) — these are noise. |
Workflow:
-
Keep the original
.jtlfor per-user failure analysis and troubleshooting - this is where your captured variable columns live -
Run the filter script to create a filtered copy
-
Generate the web report from the filtered copy
REM Filter out sub-results and unresolved variables (always recommended)
python jtl_filter.py results.jtl filtered.jtl
REM Optionally also filter by label regex (e.g., exclude username rows)
python jtl_filter.py results.jtl filtered.jtl "^(?!user\d+)"
REM Generate report from the filtered file
jmeter -g filtered.jtl -o report-folder
Impact: On a real test run with ~3.4 million rows (682 MB), filtering sub-results alone reduced the file to ~565K rows (153 MB) — an 83% reduction. This makes report generation feasible where the unfiltered file would cause JMeter to run out of memory.
Aggregate Report Columns
| Column | Meaning |
|---|---|
| Label | Sampler / Transaction Controller name |
| # Samples | Total number of requests |
| Average | Average response time (ms) |
| Median | 50th percentile response time |
| 90% Line | 90th percentile response time |
| 95% Line | 95th percentile response time |
| 99% Line | 99th percentile response time |
| Min | Fastest response |
| Max | Slowest response |
| Error % | Percentage of failed requests |
| Throughput | Requests per second |
JMeter Web Report
There are two ways to get the HTML report: add -e -o report-folder to the test run so JMeter builds it at the end, or generate it afterwards from a .jtl with jmeter -g. The second is usually the better option - it lets you filter the .jtl first, and you can regenerate as often as you like without re-running the test. Either way, open index.html in the output folder.
Report Sections
Dashboard:
- Summary statistics (total requests, error %, throughput)
- APDEX (Application Performance Index) score
- Response time over time graph
Charts:
- Response Times Over Time - shows how response times change during the test
- Active Threads Over Time - confirms the ramp-up profile
- Transactions Per Second - throughput over time
- Response Time Percentiles - distribution of response times
- Response Time vs Threads - how response time changes as load increases
Tables:
- Statistics table - per-sampler breakdown with averages, percentiles, throughput, and error rates
- Errors table - breakdown of error types and which samplers they occurred on
How Transaction Controllers Appear in the Report
With "Generate parent sample" unchecked on Transaction Controllers (see Section 6):
-
The report shows both the Transaction Controller name (e.g.,
A - Login) and each individual request inside it -
This lets you pinpoint exactly which request within a flow is slow
-
The Transaction Controller entry shows the total time for the group, while individual requests show their own times
This is why naming conventions matter - A - Login, B - Dashboard appear sorted and readable in the report, and the numbered samplers (01 - POST - Submit Login) let you drill into specifics.
Optimizing the HTML Report
The JMeter HTML report includes "over time" graphs that produce a very large graph.js file (can reach 500+ MB for long test runs). This makes the report slow to open in a browser.
To reduce report size, disable the heaviest graphs using JMeter properties:
jmeter -g filtered.jtl -o report-folder ^
-Jjmeter.reportgenerator.graph.responseTimeOverTime.enabled=false ^
-Jjmeter.reportgenerator.graph.latenciesOverTime.enabled=false ^
-Jjmeter.reportgenerator.graph.bytesThroughputOverTime.enabled=false ^
-Jjmeter.reportgenerator.graph.connectTimeOverTime.enabled=false
This disables four over-time graphs that contribute the most to graph.js size. The dashboard summary, statistics table, percentile charts, and throughput summary are preserved — these are the charts you actually need for analysis.
When regenerating from .jtl: If the original test saved results in XML format (not CSV), add
-Jjmeter.save.saveservice.output_format=csvto force CSV parsing.
This gives you two views: the full .jtl for tracking which users failed, and a clean, fast-loading report for aggregate analysis.
Identifying Bottlenecks
High Response Times
-
All requests are slow → likely a server-side issue (CPU, memory, database)
-
Specific requests are slow → focus on those endpoints. Could be a slow database query, external service call, or inefficient code
-
Response times increase over time → possible memory leak, connection pool exhaustion, or growing queue
High Error Rate
-
Errors from the start → script issue or environment problem (wrong URL, expired credentials)
-
Errors appear under load → server capacity issue (connection limits, thread pool exhaustion, timeouts)
-
Specific error codes:
500- server error, check server logs502/503- server overloaded or down429- rate limitedConnection reset / timeout- server can't accept more connections
Throughput Plateau
- Throughput stops increasing even though more users are being added
- This means the server has reached its maximum capacity
- Look at response times at this point - they'll be climbing
Response Time vs Threads
The web report includes a "Response Time vs Threads" chart. The ideal pattern:
- Response time stays flat as threads increase → server handles the load well
- Response time starts climbing at a certain thread count → that's the capacity threshold
Tips
-
Never run load tests from GUI mode - even for "quick" tests. Use CLI from the start
-
Delete or empty the report folder before each run - JMeter won't overwrite an existing report folder
-
Name your result files with the run number or timestamp (e.g.,
results-run1.jtl,results-20260212.jtl) so you don't lose previous results -
Watch the CLI output during the test - if errors spike or response times explode early, you may want to stop and investigate before wasting time on a full run
-
Run multiple iterations - one run is not enough. Run at least 2-3 times to confirm results are consistent
-
Decide what to capture before you run - add the variables you would need to diagnose a failure (username, order ID) to
sample_variablesup front. You cannot recover them from a finished.jtl, and re-running a two-hour test to find out who failed is a bad afternoon -
Always filter the .jtl before generating reports - sub-results from Transaction Controllers inflate the file by ~80-85%. Filtering first makes report generation faster and prevents out-of-memory errors on large files
-
Compare against NFRs immediately - check the 90th percentile response time, throughput, and error rate against the targets from Section 2