The JMeter HTML report is a dashboard that JMeter builds from a results file: an APDEX score per request label, a statistics table with percentiles, error tables and about 15 graphs. Generate it after a test with jmeter -n -t plan.jmx -l results.jtl -e -o report, or from an existing results file with jmeter -g results.jtl -o report, then open report/index.html in a browser.
This guide generates the report from a real 97-second JMeter 5.6.2 run and reads every table it produced. It also covers three defaults that change what the report says: percentiles that only look at the last 20,000 samples, graphs with one point per minute, and an APDEX score that counts fast errors as frustrated users.
Key Takeaways
-e -o <folder>builds the report when a test ends;-g <results file> -o <folder>builds it from an existing CSV results file with JMeter’s default columns. The folder must be empty or missing, or JMeter stops with an error.- Read the Errors table and the Statistics table first. The APDEX score folds response times and errors into one number, with thresholds of 500 ms and 1,500 ms unless you change them.
- Percentiles come from a sliding window of the last 20,000 samples. In a 30,000-sample file whose first 10,000 samples took 1 second, the report showed a 99th percentile of 100 ms.
- Over-time graphs default to one point per minute. A 97-second test drew 3 points per graph, and Hits per Second read 3 per second for its last, partial minute.
- Everything is set with
jmeter.reportgenerator.*properties, on the command line with-Jor inuser.properties, and you can regenerate a report as often as you like from the same file.
How do you generate a JMeter HTML report?
Run the test in non-GUI mode with -e and -o, or point -g at a results file you already have. Both write a static site to the output folder: index.html, a content folder with the graphs, the theme files and a statistics.json with the main table.
# run a test and build the report at the end
jmeter -n -t plan.jmx -l results.jtl -e -o report
# build a report from an existing results file
jmeter -g results.jtl -o report2
The output folder must not exist or must be empty. Pointing -o at the folder from an earlier report stopped JMeter with exit code 1:
An error occurred: Cannot write to '/.../rep/report' as folder is not empty
For a test run, add -f: it deletes the existing results file and report folder before the test starts. Without -f, JMeter also appends new results to an existing .jtl, so a rerun mixes two tests into one report. The GUI has the same generator under Tools, Generate HTML report. Our non-GUI mode guide lists the other command-line flags.
What results file does the HTML report need?
A CSV results file with JMeter’s default columns, which is what -l results.jtl writes unless you changed the save settings. The generator needs the timestamp (in milliseconds, the default format), elapsed time, label, response code and message, thread name, success flag, assertion failure message, bytes, latency, connect time and thread counts. Results saved as XML do not work.
We saved one run with jmeter.save.saveservice.output_format=xml and fed it to -g. JMeter exited with code 1 after printing that the file “does not contain the field names header” and An error occurred: Could not read sample <0>; the log had the real cause, Cannot have quote-char in plain field:[<testResults version=], which is the CSV parser meeting XML. Switch the output format back to CSV and rerun the test, because an XML results file cannot be turned into the dashboard.
If you changed jmeter.save.saveservice.* properties to save less, the “Generating Report Dashboard” chapter of the JMeter manual lists the ones the report needs. Our guide to saving JMeter results to CSV or XML covers those settings.
What is in the JMeter HTML report?
The Dashboard page holds the summary tables: test and report information, the APDEX table, a Requests Summary pie of passed and failed samples, the Statistics table, the Errors table and Top 5 Errors by Sampler. The Charts menu holds the graphs, grouped into Over Time, Throughput and Response Times.
Our test: a setUp group that logged in once, 20 threads ramping up over 30 seconds that requested a product list and an endpoint failing on every tenth call, and 5 threads posting orders, all for 90 seconds against a local API that tops out near 150 requests a second. 8,755 samples in all.
How do you read the Statistics table?
The Statistics table has one row per sampler label and a Total row. Read the error percentage, then the median and the 90th, 95th and 99th percentiles, then Transactions/s. Average, Min and Max are in the table too, but one slow sample moves an average and a maximum far more than it moves a percentile.
| Label | Samples | Error % | Median | 90th pct | 95th pct | 99th pct | Transactions/s |
|---|---|---|---|---|---|---|---|
| GET items | 3,429 | 0.00% | 224 ms | 275 ms | 292 ms | 331 ms | 38.0 |
| GET flaky | 3,422 | 9.99% | 226 ms | 276 ms | 293 ms | 337 ms | 37.9 |
| POST order | 1,903 | 0.00% | 237 ms | 291 ms | 310 ms | 367 ms | 21.1 |
| POST login | 1 | 0.00% | 431 ms | 431 ms | 431 ms | 431 ms | 2.3 |
| Total | 8,755 | 3.91% | 228 ms | 279 ms | 298 ms | 339 ms | 90.9 |
Transactions/s for each row is that label’s count divided by the time the label ran, so the login row shows 2.3 a second for a single request. The Total row divides by the whole run, ramp-up included: 90.9 a second, while the API ran near 103 a second once all 25 threads were busy. To change the three percentile columns, set aggregate_rpt_pct1, aggregate_rpt_pct2 and aggregate_rpt_pct3; they default to 90, 95 and 99.
Why do the report’s percentiles hide slow samples?
The report calculates percentiles over a sliding window of the most recent samples, set by jmeter.reportgenerator.statistic_window and 20,000 by default. In a run with more samples than that, the median and percentiles describe only the end of the test. The average, minimum and maximum still use every sample, so the table disagrees with itself.
We checked it with a synthetic results file of 30,000 samples: the first 10,000 at 1,000 ms, the next 20,000 at 100 ms. The report’s Total row read:
| Statistic | Value in the report | True value for all 30,000 samples |
|---|---|---|
| Average | 400 ms | 400 ms |
| Max | 1,000 ms | 1,000 ms |
| Median | 100 ms | 100 ms |
| 90th percentile | 100 ms | 1,000 ms |
| 99th percentile | 100 ms | 1,000 ms |
A third of the samples took ten times longer, and the 90th and 99th percentiles do not show it. On our real 8,755-sample run we set the window to 1,000: the median rose from 228 ms to 241 ms, exactly the median of the last 1,000 samples, while the first 1,000 samples (during ramp-up) had a median of 108 ms.
For any run longer than 20,000 samples, set the window to at least the number of samples, for example -Jjmeter.reportgenerator.statistic_window=1000000 on the -g command. The manual’s caution is memory: a larger window keeps more values in RAM while the report is built.
Why do the HTML report percentiles differ from the Aggregate Report?
The dashboard estimates percentiles with a different formula than the Aggregate Report listener in the GUI, and the manual says the gap shows most when few samples were taken or times are spread widely. Set the JMeter property backend_metrics_percentile_estimator=R_3 to make the dashboard use the same method as the Aggregate Report.
In our run most percentiles were identical either way. The exceptions were two 99th percentiles: POST order, 1,903 samples, where the default estimator gave 366.76 ms, R_3 gave 361 ms and a plain nearest-rank count of the results file also gave 361 ms, and the Total row, 339 ms against 338 ms. The difference is small, but it explains why two reports of the same test can disagree in the last digit.
How is APDEX calculated in the JMeter report?
APDEX scores each request from 0 to 1: satisfied responses (at or under 500 ms by default) count fully, tolerating ones (up to 1,500 ms) count half, and slower ones count zero. The score is satisfied plus half of tolerating, divided by all samples. A score of 1 means every request was satisfied.
The table names its two columns “T (Toleration threshold)” and “F (Frustration threshold)”, with 500 and 1500 in them. Our GET flaky row scored 0.900 even though every one of its responses came back in under 500 ms. The 342 failed samples counted as frustrated: 3,080 satisfied out of 3,422 is 0.900. A fast error is still a bad experience, so this is the right call, but it means an APDEX drop can be errors rather than slowness.
Change the thresholds with apdex_satisfied_threshold and apdex_tolerated_threshold, in milliseconds. apdex_per_transaction sets them per label or label pattern, for example jmeter.reportgenerator.apdex_per_transaction=POST order:800|2000. Pick thresholds that match your service level objective, not the defaults.
How do you read the Errors tables?
The Errors table groups failures by response code and message and shows each type’s share of all errors and of all samples. Top 5 Errors by Sampler shows, for each sampler, its sample count, error count and its five most common errors. Start here: the error type usually tells you whether a problem is the server, the network or the test script.
Ours had one row: 500/Internal Server Error, 342 errors, 100% of errors and 3.91% of all samples, all from GET flaky. Different types point to different causes. Non HTTP response code: java.net.SocketException and connection timeouts come from the network or an overloaded load generator. 401 and 403 usually mean a token was not extracted or expired, a correlation problem. Assertion failures show the assertion’s message.
Why do the JMeter report graphs show so few points?
The over-time graphs aggregate samples into buckets of jmeter.reportgenerator.overall_granularity milliseconds, 60,000 by default: one point per minute. A test shorter than a few minutes draws a handful of points, and the first and last buckets are partial minutes divided by a full minute.
Our 97-second run produced 3 points on every over-time graph. Hits per Second read 39.7, 103.2 and 3.0: the first and last minutes were only partly covered by the test, so their averages fell, although the test never slowed down. Regenerating the same file with a 5-second granularity drew 20 points: the ramp-up from 26 to 90 hits a second, a plateau between 82 and 108, and the final partial bucket.
jmeter -g results.jtl -o report-5s -Jjmeter.reportgenerator.overall_granularity=5000
The manual says granularity must be above 1,000 ms or the throughput graphs come out wrong. For tests under 10 minutes, 5,000 to 10,000 ms gives readable graphs. Response Time Percentiles Over Time includes successful requests only, so failures never show in it: a burst of timeouts drops out of that graph and can make it look better while the Errors table gets worse.
How do you exclude ramp-up from the JMeter HTML report?
Set jmeter.reportgenerator.start_date (and end_date if needed) when you generate the report. Samples outside the range are left out of every table and graph. The format is yyyyMMddHHmmss by default, in the time zone of the machine generating the report, and you can change it with jmeter.reportgenerator.date_format.
We regenerated our report starting about 34 seconds after the main groups began (40 seconds after the setUp login), past the 30-second ramp-up:
jmeter -g results.jtl -o report-steady -Jjmeter.reportgenerator.start_date=20261101155006
| Report | Samples | Transactions/s | Median | 90th pct |
|---|---|---|---|---|
| Whole run | 8,755 | 90.9 | 228 ms | 279 ms |
| Steady state only | 5,752 | 102.5 | 238 ms | 286 ms |
The steady-state report is the one to compare between builds: throughput is no longer diluted by the ramp-up, and latency describes the load you meant to test.
Which report settings are worth changing?
Most defaults are fine. These are the ones worth changing, in user.properties or with -J on the -g command, since a report can be rebuilt from the same results file as many times as you like:
Property (prefix jmeter.reportgenerator.) | Default | Change it when |
|---|---|---|
statistic_window | 20000 | The run has more samples than this |
overall_granularity | 60000 | The test is shorter than about 10 minutes |
apdex_satisfied_threshold, apdex_tolerated_threshold | 500, 1500 | Your targets differ |
start_date, end_date | empty | You want steady state only |
report_title | Apache JMeter Dashboard | Reports are shared |
exporter.html.series_filter | empty | Graphs are crowded with labels |
sample_filter removes samples before any calculation, while series_filter only hides rows and series from the finished report. Use the first to exclude a warm-up request from the numbers, the second to tidy the charts.
Can you get a JMeter report without running the test yourself?
Yes, as long as you have the results file. LoadFocus runs uploaded JMeter test plans from 25+ cloud regions and lets you download each region’s JTL file, the full CSV results with a header row and JMeter’s default columns, so jmeter -g can build this dashboard from a cloud run.
The JTL file analysis guide shows where the files are. LoadFocus also builds its own report for each run, with charts and tables that can be exported as a PDF; see load test reports.
Frequently Asked Questions
How do I generate a JMeter HTML report from a JTL file?
Run jmeter -g with the path to the results file and -o with an empty or new output folder, for example jmeter -g results.jtl -o report, then open index.html in that folder. The results file must be CSV with JMeter’s default columns; an XML results file fails with “Could not read sample”.
Why does JMeter say the report folder is not empty?
JMeter refuses to write the dashboard into a folder that already contains files, and stops with “Cannot write to … as folder is not empty”. Choose a new folder name, delete the old folder, or, when generating the report at the end of a test, add the -f option, which deletes the existing results file and report folder first.
What is a good APDEX score in JMeter?
A good APDEX score is one close to 1 against thresholds taken from your own response time target, because the score only means something relative to those thresholds. A score of 1 means every request was satisfied, and 0.5 can mean every request was tolerable or half were satisfied and half frustrated. Set the thresholds from your response time target first, then track the score between test runs.
Can I change the percentiles in the JMeter HTML report?
Yes. The Statistics table and percentile graphs use three percentiles set by the properties aggregate_rpt_pct1, aggregate_rpt_pct2 and aggregate_rpt_pct3, which default to 90, 95 and 99. Set them in user.properties or with -J when you generate the report.
Does the JMeter HTML report include Transaction Controller samples?
Some parts do and some do not. According to the JMeter manual, Response Times Over Time, Transactions per Second and Response Time Percentiles include Transaction Controller results, while Hits per Second, Bytes Throughput Over Time and the Requests Summary leave them out. Keep “Generate parent sample” unticked on Transaction Controllers, the default, for the most accurate figures.
Sources
- Report contents, required save settings, properties and defaults, percentile estimator note and generation commands: Apache JMeter User’s Manual, chapter 14 “Generating Report Dashboard”, as shipped with JMeter 5.6.2 and at jmeter.apache.org/usermanual/generating-dashboard.html, November 2026.
- Default values:
bin/reportgenerator.propertiesandbin/user.propertiesin Apache JMeter 5.6.2. - APDEX as an open zero-to-one standard: The Apdex Users Group, apdex.org, November 2026. The scoring of failed samples was worked out from our report’s own numbers.
- LoadFocus JTL downloads and PDF reports: loadfocus.com/docs/guides/jmeter-load-testing/load-testing-jtl-file-analysis and loadfocus.com/load-testing/reports, read 1 November 2026.
- Test runs: Apache JMeter 5.6.2 in non-GUI mode on Java 17.0.20 against a local Node.js 23.11.1 test API on an Apple M1 Max, 1 November 2026; the 30,000-sample file was built from that run’s rows with timings replaced.