Resource library

QA How-To

Run JMeter in Non-GUI Mode and Generate the HTML Report

Run a JMeter non GUI mode HTML report from CSV JTL results, verify its data, and troubleshoot common dashboard failures with this step-by-step tutorial.

19 min read | 3,016 words

TL;DR

Save a JMX plan, run `jmeter -n -t plan.jmx -l results.jtl -e -o report`, and open `report/index.html`. To build a dashboard later from the same CSV results, run `jmeter -g results.jtl -o another-empty-directory`.

Key Takeaways

  • Build and verify the JMX with tiny traffic before executing load without the GUI.
  • Use -n -t -l -e -o to save a CSV JTL and generate HTML at the end of a run.
  • Use -g with an existing JTL and a fresh output directory to rebuild a report later.
  • Keep JMeter diagnostics separate from per-sample result data.
  • Check sample count, errors, achieved rate, and workload context before interpreting charts.
  • Archive the plan, property overrides, JTL, logs, report, and environment details together.

To create a JMeter non GUI mode HTML report, start with a saved .jmx test plan and a CSV results file. Run the plan with jmeter -n -t plan.jmx -l results.jtl -e -o report, then open report/index.html. The -e option builds the dashboard after the run; -o names a new or empty directory.

This tutorial builds a small, reproducible HTTP test against a local server, executes it without the JMeter interface, creates the report both during and after execution, and checks the data behind the charts. The localhost exercise proves the workflow; its timing numbers are not evidence about a production service. Use an authorized, monitored environment before increasing load.

What You Will Build

  • A minimal .jmx plan that calls GET / on a Python server bound to your computer.
  • A non-GUI JMeter run that writes a CSV-format .jtl file and a separate diagnostic log.
  • An HTML dashboard generated at the end of a run and another generated later from the same JTL.
  • A small CSV check that confirms sample count, failures, elapsed times, and achieved request rate.
  • A repeatable artifact layout that keeps each run's inputs and outputs together.

Prerequisites

Use Apache JMeter 5.6.3 with Java 17 and Python 3.12 for the commands below. The official JMeter download page lists 5.6.3 and requires Java 8 or later; its release notes recommend Java 17 or later for the 5.6.x line. Python supplies a disposable HTTP target and parses CSV. For a different approved installation, match its supported Java runtime and avoid guessed download versions.

On macOS or Linux, place the extracted official binary distribution somewhere you control and add its bin directory to PATH. Verify the download using the checksum or signature published beside the distribution. Set JMETER_HOME to the extracted directory, then run the checks below. The examples use a POSIX shell; on Windows, use bin\jmeter.bat and PowerShell equivalents for filesystem commands.

java -version
python3 --version
export JMETER_HOME="$HOME/tools/apache-jmeter-5.6.3"
test -x "$JMETER_HOME/bin/jmeter"
"$JMETER_HOME/bin/jmeter" --version

Replace the JMETER_HOME value with your real installation path. If test -x fails, inspect the extraction path before running anything else. Use port 8000 locally and allow space for the JTL and dashboard. Run all commands from ~/jmeter-report-demo. The plan calls only 127.0.0.1.

Step 1: Start and Verify a Local HTTP Target

Start a built-in Python server in a separate terminal. Keep the original terminal, where JMETER_HOME is exported, for subsequent commands. The server lists / even without an index.html; binding to 127.0.0.1 keeps it local. Leave the server running until the test finishes.

mkdir -p "$HOME/jmeter-report-demo"
cd "$HOME/jmeter-report-demo"
python3 -m http.server 8000 --bind 127.0.0.1

Back in the original terminal, ask the server for headers. The verification command should show an HTTP 200 response. If you receive a connection error, check that the first terminal is still running and that another program has not taken port 8000.

cd "$HOME/jmeter-report-demo"
curl -sS -o /dev/null -w 'HTTP %{http_code} from %{remote_ip}:%{remote_port}\n' http://127.0.0.1:8000/

Verify: expect HTTP 200 from 127.0.0.1:8000. The directory listing is intentionally plain: its content does not require authentication, token correlation, or application setup. It is suitable for proving the JMeter command and report pipeline. Python's simple server is not a representative performance target. Keep the exercise at the specified low thread count.

For a real service, document its approved target, test window, expected traffic, and stop conditions. The load testing guide covers workload design once the reporting mechanics work.

Step 2: Create a Small, Asserted JMeter Plan

Open the JMeter interface only to author the plan. Add Threads (Users) > Thread Group beneath Test Plan. Set Number of Threads to ${__P(users,2)}, Ramp-up Period to ${__P(ramp,2)}, and Loop Count to ${__P(loops,3)}. These expressions read local properties from the command line and provide safe defaults if no override is supplied. Name the group Local smoke users.

Under that Thread Group, add Config Element > HTTP Request Defaults. Set Protocol to http, Server Name or IP to ${__P(host,127.0.0.1)}, and Port Number to ${__P(port,8000)}. Add Sampler > HTTP Request under the group, name it GET local index, choose GET, and set Path to /. Leave the sampler's server fields blank so it inherits the defaults. Then add Assertions > Response Assertion as a child of the HTTP Request. Select Response Code, choose Equals, and enter 200 as the pattern. A successful socket exchange should not silently count a 404 as a successful exercise.

Add Timer > Constant Timer under the Thread Group and set Thread Delay to 250 milliseconds. It slows the tiny example enough for a short time-series chart while keeping the run quick. Put the timer at group scope so it affects the single sampler. Do not add View Results Tree to the saved load plan; that listener is useful while debugging a single user but retains sample details in memory. Save the plan as local-smoke.jmx in the working directory.

cd "$HOME/jmeter-report-demo"
test -s local-smoke.jmx
python3 - <<'PYXML'
from pathlib import Path
import xml.etree.ElementTree as ET
root = ET.parse(Path('local-smoke.jmx')).getroot()
tags = {node.tag for node in root.iter()}
for required in ('ThreadGroup', 'HTTPSamplerProxy', 'ResponseAssertion', 'ConstantTimer'):
    assert required in tags, f'Missing {required}'
print('Plan XML contains thread group, HTTP sampler, assertion, and timer')
PYXML

Verify: expect the four-element confirmation. The next step checks behavior. For more patterns, see JMeter assertions and listeners and JMeter timers and pacing.

Step 3: Run a Non-GUI Shakeout and Inspect the JTL

A shakeout checks that JMeter can load the JMX, reach the server, execute the assertion, and write a usable results file. Use -n for CLI mode, -t for the plan, -l for sample results, and -j for JMeter's diagnostic log. These last two files serve different purposes: the JTL has per-sample measurements, while the log contains startup, configuration, and runtime messages. Pass -J values to override the plan's __P defaults without editing XML.

cd "$HOME/jmeter-report-demo"
mkdir -p runs
"$JMETER_HOME/bin/jmeter" -n -t local-smoke.jmx \
  -Jusers=1 -Jramp=1 -Jloops=2 \
  -Jhost=127.0.0.1 -Jport=8000 \
  -l runs/shakeout.jtl -j runs/shakeout.log

Run this once with new output paths. JMeter may refuse to reuse an existing results file or mix new samples with old ones depending on invocation and configuration. Keep one JTL and one log per test run; do not use -f to delete prior artifacts merely to make a rerun convenient. If you repeat the shakeout, choose a new basename.

python3 - <<'PYCSV'
import csv
from pathlib import Path
path = Path('runs/shakeout.jtl')
with path.open(newline='') as handle:
    rows = list(csv.DictReader(handle))
assert len(rows) == 2, f'Expected 2 samples, got {len(rows)}'
assert all(row['label'] == 'GET local index' for row in rows)
assert all(row['responseCode'] == '200' and row['success'].lower() == 'true' for row in rows)
print(f'{len(rows)} HTTP 200 samples passed the JMeter assertion')
PYCSV

Verify: expect two passing samples and CSV columns timeStamp, elapsed, label, responseCode, and success. For XML output or missing columns, check jmeter.save.saveservice.output_format and the official results-file properties.

For failed samples, read runs/shakeout.log, JTL response codes, and the server terminal. Report generation cannot repair invalid traffic. The JMeter tutorial for beginners explains the relationship between samplers, assertions, and properties if the plan tree is unfamiliar.

Step 4: Generate a JMeter Non GUI Mode HTML Report at the End of a Run

Now run the same plan with two users and three iterations each. Add -e to request dashboard generation when the test finishes and -o to select its destination. Use a directory name that does not exist or is empty. Apache's dashboard manual documents both options and the empty-output-directory requirement. The report uses the JTL generated by this run; it is not an alternative to keeping the raw data.

cd "$HOME/jmeter-report-demo"
"$JMETER_HOME/bin/jmeter" -n -t local-smoke.jmx \
  -Jusers=2 -Jramp=2 -Jloops=3 \
  -Jhost=127.0.0.1 -Jport=8000 \
  -l runs/inline.jtl -j runs/inline.log \
  -e -o runs/report-inline

The command generates six samples: two users times three loops. The timer adds 250 milliseconds before each sampler call. Six results are too few for meaningful percentiles but prove the reporting path.

test -s runs/inline.jtl
test -s runs/report-inline/index.html
python3 - <<'PYCOUNT'
import csv
with open('runs/inline.jtl', newline='') as handle:
    rows = list(csv.DictReader(handle))
assert len(rows) == 6, f'Expected 6 samples, got {len(rows)}'
print(f'{len(rows)} samples and HTML index found')
PYCOUNT

Verify: the final line reports six samples, and runs/report-inline/index.html exists. Open that file in a browser through your file manager, or serve the working directory locally and visit the report path. The report's Statistics and Errors tables should reflect your JTL. If the server was stopped during the run, the dashboard may still generate but will correctly show failed requests. A generated HTML file alone does not certify success.

For real scenarios, choose duration and arrival patterns from the service objective. Thread count does not equal request rate; compare achieved throughput with the intended workload.

Step 5: Regenerate a Dashboard From an Existing Results File

You may need to rebuild an HTML dashboard after a CI job has finished, after transferring a JTL to a review machine, or after changing report configuration. Use -g with an existing CSV JTL and -o with a different empty directory. This command does not execute the test plan, contact the target, or change samples. It reprocesses saved results. Keep the original JTL untouched so another analyst can reproduce the view.

cd "$HOME/jmeter-report-demo"
"$JMETER_HOME/bin/jmeter" -g runs/inline.jtl \
  -o runs/report-regenerated \
  -j runs/regenerate.log
test -s runs/report-regenerated/index.html
python3 - <<'PYREPORT'
from pathlib import Path
for folder in ('runs/report-inline', 'runs/report-regenerated'):
    html = Path(folder, 'index.html').read_text(encoding='utf-8')
    assert 'Apache JMeter' in html or 'JMeter' in html
    print(f'{folder}: {len(html)} index characters')
PYREPORT

Verify: both report directories contain an HTML index. Their surrounding generated files may differ in metadata, timestamps, or report settings, but their source sample set is the same runs/inline.jtl. Compare the Statistics table, total sample count, and error count rather than expecting byte-for-byte identical output.

Regeneration needs the required CSV columns. Restore compatible save-service settings and produce a new JTL if fields are missing. Compare dashboards using the same JMeter release and report properties; filters and APDEX thresholds change the view. Archive those settings.

Step 6: Read the Data Behind the Dashboard

Inspect Statistics, Errors, Response Times Over Time, and Active Threads Over Time in index.html. Statistics groups by sampler label; distinct business operations need distinct names. The dashboard documentation defines its graphs, APDEX, and filters.

Use this Python check to calculate a few simple values directly from the CSV. It reads columns by header name, so it does not rely on a fixed column order. The nearest-rank p95 calculation is shown for transparency, not as a claim that it matches JMeter's dashboard percentile estimator. With only six samples, p95 is especially unstable.

python3 - <<'PYMETRICS'
import csv
import math
from pathlib import Path
with Path('runs/inline.jtl').open(newline='') as handle:
    rows = list(csv.DictReader(handle))
required = {'timeStamp', 'elapsed', 'label', 'responseCode', 'success'}
assert rows and required.issubset(rows[0]), 'Missing dashboard input columns'
elapsed = sorted(int(row['elapsed']) for row in rows)
failures = [row for row in rows if row['success'].lower() != 'true']
start = min(int(row['timeStamp']) for row in rows)
end = max(int(row['timeStamp']) + int(row['elapsed']) for row in rows)
seconds = max((end - start) / 1000, 0.001)
p95 = elapsed[math.ceil(0.95 * len(elapsed)) - 1]
print(f'samples={len(rows)} failures={len(failures)}')
print(f'elapsed_ms_min={elapsed[0]} p95_nearest_rank={p95} max={elapsed[-1]}')
print(f'observed_completion_rate_per_second={len(rows) / seconds:.2f}')
PYMETRICS

Verify: samples=6 and failures=0 when the server and assertion worked. Expect timing and rate to vary by machine. The rate formula is a crude exercise check. JMeter timestamps default to completion time; use the documented setting and a proper measurement window for production analysis.

Read errors before celebrating low latency. A service that returns an error quickly can have an attractive response-time chart and still fail the objective. Compare achieved rate, error percentage, p95 or p99, and server-side CPU, memory, queue, and database metrics over the same window. JMeter observes client-side protocol behavior; it cannot identify a database bottleneck on its own. For multi-step workflows, use JMeter CSV data set configuration to supply distinct data and avoid accidental account contention.

Step 7: Keep Runs Reproducible and Prepare for CI

A reviewable result needs the plan, command, JTL, report, JMeter log, environment, and acceptance criteria. Keep each run in its own directory. The following command copies the plan into a new run folder and writes the exact CLI settings into a small text manifest. It uses a timestamp plus the shell process ID to reduce accidental folder collisions; mkdir still fails safely if a path exists. Review the manifest before replacing localhost values with an approved test target.

cd "$HOME/jmeter-report-demo"
run_dir="runs/run-$(date -u +%Y%m%dT%H%M%SZ)-$"
mkdir "$run_dir"
cp local-smoke.jmx "$run_dir/plan.jmx"
cat > "$run_dir/manifest.txt" <<'MANIFEST'
Target: 127.0.0.1:8000
Plan: plan.jmx
Users: 2
Ramp seconds: 2
Loops per user: 3
Expected samples: 6
Purpose: reporting pipeline exercise, not a capacity claim
MANIFEST
"$JMETER_HOME/bin/jmeter" -n -t "$run_dir/plan.jmx" \
  -Jusers=2 -Jramp=2 -Jloops=3 \
  -Jhost=127.0.0.1 -Jport=8000 \
  -l "$run_dir/results.jtl" -j "$run_dir/jmeter.log" \
  -e -o "$run_dir/report"
test -s "$run_dir/plan.jmx"
test -s "$run_dir/results.jtl"
test -s "$run_dir/report/index.html"
test -s "$run_dir/manifest.txt"

Verify: in the same shell session, all four checks exit successfully. The final report remains beside the exact plan that produced it. If you move this pattern into CI, use an isolated directory for each job, publish the JTL and log alongside HTML, and configure retention before collecting large load-test data. Save JMeter and Java versions, source commit, input-data revision, target build, test window, and dashboard properties in the manifest for a real run.

When multiple generators are necessary, the JMeter distributed testing guide covers matching versions, files, and remote configuration. This localhost exercise establishes no application capacity.

Step 8: Check a CI Smoke Run Against Explicit Conditions

JMeter's process exit does not by itself enforce your application's latency or error budget. For this fixed six-request exercise, fail a CI step if the result set is incomplete or any sample failed. Pass the Step 7 JTL path to Python in the same shell session.

python3 - "$run_dir/results.jtl" <<'PYGATE'
import csv
import sys
with open(sys.argv[1], newline='') as handle:
    rows = list(csv.DictReader(handle))
assert len(rows) == 6, f'Incomplete run: {len(rows)} samples'
failed = [row for row in rows if row['success'].lower() != 'true']
assert not failed, f'{len(failed)} failed samples'
print('Smoke conditions passed: 6 samples, 0 failures')
PYGATE

Verify: the command prints the passing message and exits zero. Stop the Python server and rerun with a fresh run directory to see a failure. Real performance gates need pre-agreed per-transaction thresholds, a controlled environment, enough observations, and separate generator-health checks. Keep a short smoke gate focused on plan correctness; do not infer capacity from it.

JMeter Non GUI Mode HTML Report: Options and Artifact Roles

Option or artifact Role Common mistake
-n Execute without the GUI Expecting a browser window to appear
-t local-smoke.jmx Select the saved plan Passing a directory or unsaved GUI state
-Jusers=2 Set a local JMeter property Assuming it changes an unrelated hard-coded field
-l results.jtl Save per-sample results Treating the JTL as the diagnostic log
-j jmeter.log Save JMeter diagnostics Expecting it to contain report metrics
-e -o report Build HTML after a run Reusing a nonempty report directory
-g results.jtl -o report2 Build HTML from saved CSV Expecting the test plan to run again

The core commands are documented in Apache JMeter's CLI reference and dashboard guide. -f exists to force deletion of result files and a report folder, but it is unsuitable when you need an audit trail. Unique paths make a rerun safer and preserve comparisons. File extensions are conventions: the actual save-service format determines whether a .jtl contains CSV or XML. Confirm the header before using -g.

The report answers only questions represented by the plan. Missing transactions, absent assertions, and saturated generators can all mislead. Interpret the dashboard with workload notes and server telemetry.

Troubleshooting

Problem: Connection refused or a non-200 sample -> Check the Python server terminal, run the Step 1 curl command, and confirm -Jhost and -Jport. If the response is 404, inspect the sampler path and server working directory. Do not change the assertion to accept a bad status merely to make the graph green.

Problem: JMeter says the output directory is not empty -> Select a fresh -o path such as runs/report-inline-2, or intentionally empty an old disposable directory after preserving its contents elsewhere. The same rule applies to -g reports. Avoid -f when previous results matter.

Problem: the dashboard is blank or reports missing columns -> Open the JTL as text and confirm it is CSV with a header, timestamps, elapsed time, label, response code, success, and default report fields. Check custom jmeter.save.saveservice.* settings against the dashboard manual. Generate a new result file after correcting them.

Problem: the expected six samples are not present -> Check the Thread Group's loop and user fields, look for a scheduler or error action that stops threads, and read jmeter.log. The CSV count is a better check than assuming every user completed its plan. A failed sampler usually still has a result row.

Problem: the HTML report exists but shows errors -> Inspect the Errors table and the JTL's responseCode and success columns. A successful report-generation process only means JMeter could process the file. It does not mean the target met a functional or performance objective.

Problem: graphs or percentiles seem strange -> Six samples are too few for stable tails. Check filters and APDEX settings, then compare a longer authorized run with matching labels, time windows, and server telemetry.

Interview Questions and Answers

Q: Why run JMeter without its GUI for load testing?

The GUI and visual listeners consume generator CPU and memory, which can reduce load capacity or distort timings. CLI mode is scriptable and gives consistent paths for raw results and logs. I use the GUI to build and debug a plan with minimal traffic, then execute measured runs with -n.

Q: What is the difference between -l and -j?

-l writes per-sample measurements for analysis and HTML generation. -j controls JMeter's diagnostic log. I keep both because a malformed plan or runtime warning may be visible in the log while the JTL explains request outcomes.

Q: How do you build a report after the run has ended?

Use jmeter -g results.jtl -o new-empty-directory with a compatible CSV results file. This does not rerun the JMX or contact the service. I keep the original JTL and report properties so the view can be reproduced.

Q: Why can a dashboard show very fast requests while the test failed?

A server may return errors quickly, or an assertion may flag a body that is wrong despite a normal HTTP exchange. I inspect the error count, response codes, and business assertions before latency. Fast failed work is not successful throughput.

Q: Does setting two threads guarantee a particular request rate?

No. Threads represent concurrent execution paths. Response time, timer delays, loop structure, and ramp-up determine observed completions. I compare the measured rate and active-thread graph with the intended workload.

Q: What information should accompany an HTML report?

The JTL, JMX revision, JMeter version, Java runtime, property overrides, input data, environment build, and run window make the report interpretable. I also record target load, acceptance criteria, and generator and server monitoring. A standalone chart cannot establish the cause of a regression.

Common Mistakes

  • Running meaningful load in the GUI with View Results Tree enabled.
  • Reusing a report directory or appending a new run to an old results file.
  • Treating jmeter.log as the data source for the HTML dashboard.
  • Removing CSV save-service fields required by report generation.
  • Skipping assertions and mistaking quick error responses for good performance.
  • Reporting only a global average instead of per-operation percentiles, errors, and achieved traffic.
  • Inferring production capacity from a local Python server or a six-sample demonstration.
  • Losing the exact JMX and property overrides used for a published chart.

Conclusion

A correct JMeter report begins with valid samples and a traceable run. Use a fresh JTL and report directory, inspect failures before latency, and retain the plan and settings. The localhost exercise verifies the mechanics; your next run should use an approved, monitored environment.

Where To Go Next

You now have a reproducible JMeter non-GUI mode HTML report workflow: validate a plan, save a CSV JTL, generate HTML at completion, regenerate it later, and check the underlying samples. Repeat the sequence with an authorized service and a workload based on observed traffic. Add representative input data, business assertions, longer steady-state periods, and synchronized server telemetry before making performance claims.

Use the JMeter thread groups guide to model concurrency and ramp-up deliberately. The performance testing roadmap helps place this reporting step inside a broader test strategy. Keep the JTL beside the dashboard so every conclusion can be traced to the samples that produced it.

Interview Questions and Answers

Explain the JMeter CLI flags needed for an HTML report.

`-n` runs without the GUI, `-t` selects the JMX, and `-l` writes results. `-e` generates a dashboard after the run, while `-o` names its empty destination. I also set `-j` to capture diagnostics separately.

How would you regenerate a dashboard without rerunning a load test?

I use `jmeter -g results.jtl -o fresh-report-path`. I first confirm that the JTL is CSV and retains the columns the dashboard requires. The command processes saved samples and does not contact the target.

Why should each JMeter run use unique JTL and report paths?

Unique paths prevent old and new samples from being mixed and avoid the dashboard empty-directory error. They also preserve an audit trail for comparison. I store the plan, logs, and run settings in the same run folder.

How do you confirm a JMeter run produced valid load?

I check expected versus actual sample count, response codes, assertion failures, achieved rate, and active threads. Then I compare those with the workload design and load-generator health. A completed command or generated HTML page alone is insufficient.

What can make the HTML dashboard statistics misleading?

Tiny sample sets, mixed sampler labels, failed requests, missing assertions, warm-up periods, and report filters can all change interpretation. Generator saturation and unrepresentative targets add further uncertainty. I state the test window and review raw JTL data with server metrics.

Why should JMeter load tests run in CLI mode?

GUI rendering and visual listeners consume resources that the generator needs to issue requests. CLI runs are easier to automate and reproduce with a recorded command and artifacts. I still use the GUI for authoring and low-volume debugging.

What does `-Jusers=2` do in a JMeter plan?

It sets a local JMeter property named `users`. It changes thread count only if the plan reads that property, for example with `${__P(users,2)}` in the Thread Group. It does not rewrite a hard-coded thread count automatically.

Frequently Asked Questions

How do I run JMeter in non-GUI mode and generate an HTML report?

Run `jmeter -n -t plan.jmx -l results.jtl -e -o report` with a saved plan and a new or empty report directory. JMeter executes the plan, saves sample results, and builds `report/index.html` after the run.

Can JMeter generate an HTML dashboard from an existing JTL?

Yes. Use `jmeter -g results.jtl -o new-report-directory`. The JTL should contain compatible CSV fields, and the output directory must be empty or absent.

What is the difference between a JTL file and jmeter.log?

A JTL contains sample data such as timestamp, elapsed time, label, status, and response code. `jmeter.log` contains JMeter diagnostic messages. The dashboard generator reads the JTL, not the diagnostic log.

Why does JMeter reject my HTML report directory?

The dashboard generator requires a new or empty destination. Choose a unique path for each run so old evidence is preserved and report files are not mixed.

Does a generated HTML report mean my performance test passed?

No. HTML generation only proves that JMeter processed the results file. Check failed samples, assertions, achieved traffic, latency targets, and server telemetry against criteria defined before the test.

Can an XML JTL be used with the HTML dashboard?

The standard dashboard workflow expects CSV sample results with required fields. Check `jmeter.save.saveservice.output_format` and regenerate the test results as CSV if you changed the default to XML.

Is localhost a useful target for performance conclusions?

Localhost is useful for verifying a command, plan, JTL, and report pipeline. It does not model production network, application, or infrastructure behavior, so use an authorized representative environment for capacity conclusions.

Related Guides