Running Tests in CLI Mode
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. |
The reference material consulted while preparing these pages is listed in the section bibliography.
Every real load test in JMeter runs from the command line. The GUI builds and debugs the plan; the CLI (-n) runs it,
writes the results file, prints a running summary, generates the HTML dashboard and returns control to a shell or a CI
job. This page covers that run in depth: every command-line option, how one plan serves many load profiles, how to read
the live summariser, how to stop a run cleanly, what the process exit code does and does not tell you, and how big the
machine that generates the load has to be.
It was written against Apache JMeter 5.6.3, the latest release at the time of writing, run on Java 21 (5.6.x needs
Java 8 or newer; this guide recommends 17 or newer). The option table was checked against the user manual and against
the jmeter -? output of a 5.6.3 installation, and the exit codes and the shutdown behaviour were measured on that
installation and read from the 5.6.3 source code. JMeter 6.0 is not released; where the development branch plans a
change that affects command-line runs (Java 17 as the minimum, Log4j start-up warnings, numeric property parsing), the
text says "planned". Linux tuning values come from the kernel documentation (ip-sysctl), and JVM defaults from the
Java 21 java command reference. The books the section draws on call this mode "non-GUI mode" (Pro Apache JMeter
ch. 3, JMeter 3.0); the manual has called it CLI mode since 5.1.
For installing JMeter and the launcher environment variables (HEAP, GC_ALGO, JVM_ARGS, setenv.sh) see
Installation & Configuration. For the properties mechanism
behind -J see Properties, Variables & Functions. The
results file and the dashboard that a CLI run produces are covered in
Listeners & Results Files and
HTML Dashboard Report.
Why load tests run in CLI mode
The manual’s wording is direct: for load testing you must run JMeter without the GUI to get optimal results, and GUI mode is for creating the script. The reasons are mechanical, not stylistic.
| Concern | GUI mode | CLI mode (-n) |
|---|---|---|
What the JVM spends time on |
Swing rendering, tree updates, listeners refreshing tables and graphs |
Samplers, timers and writing the results file |
Memory |
Listeners such as View Results Tree keep sample results, often with bodies, in memory |
Results stream to the JTL file; nothing accumulates unless a listener in the plan keeps it |
Repeatability |
Clicks, open windows and a desktop session |
One command line, checked into version control, identical on every run |
Automation |
Not scriptable |
Runs in a shell, a container, a CI job or over SSH; produces files and an exit code |
Remote control |
Toolbar buttons |
|
The best-practices chapter’s list for reducing resource use starts with exactly this command
(jmeter -n -t test.jmx -l test.jtl), followed by: use as few listeners as possible (with -l they can all be deleted
or disabled), never use View Results Tree or View Results in Table during the load test, use CSV rather than XML output,
save only the data you need and use as few assertions as possible. A CLI run with a plan full of GUI listeners still
pays for those listeners, so the plan has to be lean as well.
Anatomy of a CLI run
The sequence below shows what happens between pressing Enter and the shell prompt coming back. The numbered parts are explained in the rest of the page.
The basic command
A complete run of the Bookshelf load profile, producing the results file, a run log and the HTML dashboard:
jmeter -n -t bookshelf-load.jmx \
-l results/bookshelf-load.jtl \
-j results/bookshelf-load.log \
-Jthreads=50 -Jrampup=60 -Jduration=660 \
-f -e -o results/report
-
-nruns without the GUI. -
-tnames the plan. -
-lnames the sample results file (JTL). Its format comes fromjmeter.save.saveservice.output_format(CSV by default), not from the file extension. -
-jnames the JMeter run log, which is otherwisejmeter.login the current directory. -
-Jsets JMeter properties that the plan reads with${__P(threads,1)}and friends. -
-fdeletes an existing results file and report folder before starting. -
-egenerates the dashboard at the end, into the folder given by-o, which must not exist or must be empty.
On Windows the same command runs through jmeter.bat (or jmeter from bin), with ^ instead of \ for line
continuation. The manual also documents drag-and-drop helpers: jmeter-n.cmd runs a dropped .jmx file in CLI mode
and jmeter-n-r.cmd runs it remotely.
Command-line options: the full list
jmeter -? (or jmeter --?) prints every option and exits. In zsh, quote the question mark (jmeter '-?'), otherwise
the shell treats it as a glob and stops with "no matches found". The table lists every option printed by 5.6.3, in the
same order, with what it does in practice. The manual’s own list (section 1.4.8 of Getting Started) has the same
options except -E, which is documented with the proxy options in section 1.4.3 and is present in the -? output.
| Short | Long | Argument | Meaning and notes |
|---|---|---|---|
|
Print the command-line options and exit (exit code 0). |
||
|
|
Print usage information and exit. |
|
|
|
Print the version banner and exit. Useful at the start of every CI job to record the exact version in the log. |
|
|
|
file |
Use this file instead of |
|
|
file |
Load an additional JMeter properties file. May be repeated. A missing file is only a warning in the run log. |
|
|
file |
The |
|
|
file |
The sample results file (JTL). If the file exists and |
|
|
file |
The Log4j 2 configuration file to use instead of |
|
|
file |
The JMeter run log. Default |
|
|
Run in CLI mode. Without it, JMeter opens the GUI (and loads the |
|
|
|
Run as a JMeter server (the |
|
|
|
scheme |
Scheme of the outbound proxy, for non-HTTP proxies (optional). |
|
|
host |
Outbound proxy host. Sets |
|
|
port |
Outbound proxy port. Sets |
|
|
host list |
Hosts that bypass the proxy, separated by a vertical bar. Sets |
|
|
user |
User name for proxy authentication. |
|
|
password |
Password for proxy authentication. The manual warns that command-line parameters may be visible to other users of the
system, so prefer system properties in a protected file ( |
|
|
name=value |
Define a JMeter property, read in the plan with |
|
|
name=value or file |
Define a JMeter property that is sent to every remote server, or (without |
|
|
name=value |
Define a Java system property, for example |
|
|
file |
Load an additional system properties file. May be repeated. |
|
|
Delete existing results files and the |
|
|
|
[category=]level |
Override a log level, for example |
|
|
Start the remote servers listed in the |
|
|
|
host list |
Start these remote servers (comma separated), overriding |
|
|
directory |
The JMeter home directory to use. |
|
|
Exit the remote servers at the end of the test (CLI mode). |
|
|
|
results file |
Generate the dashboard from an existing results file, without running a test. Needs |
|
|
Generate the dashboard after the load test. Needs |
|
|
|
folder |
Output folder of the dashboard. It must not exist or must be empty, unless |
The 5.6.3 output that the table was checked against, captured on a fresh installation (banner trimmed):
jmeter '-?'
--?
print command line options and exit
-h, --help
print usage information and exit
-v, --version
print the version information and exit
-p, --propfile <argument>
the jmeter property file to use
-q, --addprop <argument>
additional JMeter property file(s)
-t, --testfile <argument>
the jmeter test(.jmx) file to run. "-t LAST" will load last
used file
-l, --logfile <argument>
the file to log samples to
-i, --jmeterlogconf <argument>
jmeter logging configuration file (log4j2.xml)
-j, --jmeterlogfile <argument>
jmeter run log file (jmeter.log)
-n, --nongui
run JMeter in nongui mode
-s, --server
run the JMeter server
-E, --proxyScheme <argument>
Set a proxy scheme to use for the proxy server
-H, --proxyHost <argument>
Set a proxy server for JMeter to use
-P, --proxyPort <argument>
Set proxy server port for JMeter to use
-N, --nonProxyHosts <argument>
Set nonproxy host list (e.g. *.apache.org|localhost)
-u, --username <argument>
Set username for proxy server that JMeter is to use
-a, --password <argument>
Set password for proxy server that JMeter is to use
-J, --jmeterproperty <argument>=<value>
Define additional JMeter properties
-G, --globalproperty <argument>=<value>
Define Global properties (sent to servers)
e.g. -Gport=123
or -Gglobal.properties
-D, --systemproperty <argument>=<value>
Define additional system properties
-S, --systemPropertyFile <argument>
additional system property file(s)
-f, --forceDeleteResultFile
force delete existing results files and web report folder if
present before starting the test
-L, --loglevel <argument>=<value>
[category=]level e.g. jorphan=INFO, jmeter.util=DEBUG or com
.example.foo=WARN
-r, --runremote
Start remote servers (as defined in remote_hosts)
-R, --remotestart <argument>
Start these remote servers (overrides remote_hosts)
-d, --homedir <argument>
the jmeter home directory to use
-X, --remoteexit
Exit the remote servers at end of test (non-GUI)
-g, --reportonly <argument>
generate report dashboard only, from a test results file
-e, --reportatendofloadtests
generate report dashboard after load test
-o, --reportoutputfolder <argument>
output folder for report dashboard
Files: plan, results, run log
-
Results files append. Running twice with the same
-lfile and no-fappends the second run to the first, without a second header line; a dashboard generated from that file then mixes two runs. Either pass-for give every run its own file name. -
The report folder must be empty. With
-e -o reportand a non-emptyreport, 5.6.3 stops before the test starts withCannot write to '...' as folder is not emptyand exit code 1.-fempties it first. -
-eneeds-l. Without a results file there is nothing to build the dashboard from; JMeter printsIncorrect Usage:Option -e requires -l optionand the usage text. -
Date patterns in the run log name. If the
-jname contains a pattern in paired single quotes, the text between the quotes is kept literally and the rest is formatted as a date, so-j 'bookshelf_'yyyyMMdd-HHmmss'.log'writes a new log per run. This applies to the run log only, not to-l; for results files, build the name in the shell. -
LAST.-t LAST,-j LASTand-l LASTrefer to the last plan used in GUI mode, which is convenient on a workstation and meaningless on a CI agent.
stamp=$(date +%Y%m%d-%H%M%S)
jmeter -n -t bookshelf-load.jmx \
-l "results/bookshelf-load-$stamp.jtl" \
-j "results/bookshelf-load-$stamp.log" \
-e -o "results/report-$stamp"
Properties: -J, -q, -D, -S, -p and -G
The command line is processed after jmeter.properties, user.properties and system.properties have been loaded.
-q, -S, -J, -D, -G and -L are then applied in the order they appear on the command line, so a later
option overrides an earlier one. That makes this pattern safe: a profile file with defaults, then individual overrides.
# profile file first, then a one-off override: threads=80 wins
jmeter -n -t bookshelf-load.jmx -q profiles/load.properties -Jthreads=80 -l results.jtl
Two details matter in scripts. An empty value (-Jthreads=) removes the property, so the plan falls back to the
default in ${__P(threads,1)}. And in 5.6.3 numeric properties are parsed as written, so a value with a trailing
space copied into a properties file can fail to parse; trimming whitespace when parsing numeric properties is a
planned 6.0 change in changes.xml on master.
-D and -S set Java system properties, which configure the JVM and libraries rather than the plan: TLS keystores,
java.net.preferIPv4Stack, proxy settings. -G is the distributed-testing counterpart of -J: the controller sends
the properties to every server, which is how one command line sets threads on four generators at once (the page on
distributed testing covers it).
Logging: -L, -i and -j
-L changes log levels for one run without editing log4j2.xml:
# debug the HTTP sampler only, for a one-thread smoke run
jmeter -n -t bookshelf-load.jmx -l smoke.jtl -Jthreads=1 -Jduration=30 \
-Lorg.apache.jmeter.protocol.http=DEBUG
-i points to another log4j2.xml entirely. Never leave DEBUG logging on in a load run: every sample then writes
several log lines, and the disk becomes part of the measurement.
Outbound proxy: -H, -P, -N, -E, -u, -a
These options are for a load generator that reaches the system under test through a corporate proxy. The manual’s
example is jmeter -E https -H my.proxy.server -P 8000 -u username -a password -N localhost; the non-proxy list uses a
vertical bar between entries and accepts wildcards such as *.apache.org. Testing through a shared proxy measures
the proxy as much as the application, so for a load test the generator should normally sit where it reaches the
target directly. The HTTP(S) Test Script Recorder is a different thing: it is JMeter acting as a proxy for a browser
while recording.
Reports: -e, -o and -g
-e -o generates the dashboard at the end of the run. -g generates it later from any results file, which is the way
to rebuild a report after a run was killed, after changing reportgenerator.properties, or from a file merged from
several generators:
jmeter -g results/bookshelf-load.jtl -o results/report-rebuilt
Remote runs: -r, -R, -G, -X and -s
-r and -R turn the CLI process into the controller of a distributed test; -X shuts the servers down at the end;
-s (or the jmeter-server script) starts a server. The manual’s form is
jmeter -n -t testplan.jmx -r [-Gprop=val] [-Gglobal.properties] [-X]. RMI, keystores and the "every server runs the
whole plan" rule are the subject of the page on distributed testing.
Parameterised profiles: one plan, many runs
The section’s convention is one plan, bookshelf-load.jmx, whose Thread Group reads its shape from properties. Each
profile is then a command line or a small properties file, never a copy of the plan.
| Property | Read in the plan as | Default | Meaning |
|---|---|---|---|
|
|
1 |
Number of threads (virtual users) |
|
|
1 |
Ramp-up period in seconds |
|
|
60 |
Scheduler duration in seconds; it includes the ramp-up |
|
|
|
Target of HTTP Request Defaults |
The Thread Group part of the plan (Loop Count set to Infinite, so that the duration ends the test):
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Bookshelf users">
<stringProp name="ThreadGroup.num_threads">${__P(threads,1)}</stringProp>
<stringProp name="ThreadGroup.ramp_time">${__P(rampup,1)}</stringProp>
<boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
<boolProp name="ThreadGroup.scheduler">true</boolProp>
<stringProp name="ThreadGroup.duration">${__P(duration,60)}</stringProp>
<stringProp name="ThreadGroup.delay"></stringProp>
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
<elementProp name="ThreadGroup.main_controller" elementType="LoopController"
guiclass="LoopControlPanel" testclass="LoopController">
<intProp name="LoopController.loops">-1</intProp>
<boolProp name="LoopController.continue_forever">false</boolProp>
</elementProp>
</ThreadGroup>
The thread count and ramp-up go in stringProp elements because they hold a function call. A hand-edited plan that
puts ${__P(threads,1)} into an intProp fails to load in 5.6.3 with a
NumberFormatException: For input string: "${__P(threads,1)}" and exit code 1, before a single thread starts. The GUI always writes the right element type, so
the error only appears in generated or hand-edited .jmx files.
The profiles from the section, as profile files kept next to the plan:
# profiles/smoke.properties: one user, one minute, every request checked
threads=1
rampup=1
duration=60
summariser.interval=10
# profiles/load.properties: 50 users, 60 s ramp-up, 10-minute hold
threads=50
rampup=60
duration=660
# profiles/soak.properties: same 50 users held for 4 hours
threads=50
rampup=60
duration=14460
summariser.interval=60
jmeter -n -t bookshelf-load.jmx -q profiles/smoke.properties -l smoke.jtl -f
jmeter -n -t bookshelf-load.jmx -q profiles/load.properties -l load.jtl -f -e -o load-report
jmeter -n -t bookshelf-load.jmx -q profiles/soak.properties -l soak.jtl -f -e -o soak-report
Worked numbers for the load profile. 50 threads over 60 s is 0.83 new threads per second. Each Bookshelf user waits 3 s between requests and a request takes about 0.2 s, so one thread does 1 / 3.2 ≈ 0.31 requests per second and 50 threads about 15.6 requests per second once all are running. The duration of 660 s holds full load for 600 s, about 15.6 × 600 ≈ 9,400 requests in the hold, plus roughly 470 during the ramp-up. The soak profile at the same rate runs 14,400 s of hold, about 225,000 requests, which is why its summariser interval is raised to 60 s: 240 lines instead of 480. The stress, spike and arrival-rate profiles use other thread groups and are described in Custom Thread Groups & Load Shaping and Open Model & Throughput Control; they are run with exactly the same command-line pattern.
Reading the live summariser
In CLI mode JMeter adds a summariser named summary to every run (since 2.11). It prints two kinds of lines:
-
summary +lines cover only the last interval and end with the thread countersActive,StartedandFinished; -
summary =lines are running totals since the start, printed after a+line whenever they differ from it.
A real run
The output below is genuine: a 5.6.3 installation on Java 21, running a one-request plan (cli-smoke.jmx, a Thread
Group as above with one HTTP Request and a 500 ms Constant Timer) against a static file served locally by
python3 -m http.server. The summariser interval was lowered to 10 s so that a 40-second run shows several lines.
# terminal 1: a tiny local target serving www/api/books
cd www && python3 -m http.server 8080 --bind 127.0.0.1
# terminal 2
jmeter -n -t cli-smoke.jmx -l run1.jtl -j run1.log \
-Jthreads=5 -Jrampup=5 -Jduration=40 -Jsummariser.interval=10 \
-e -o report1
Creating summariser <summary>
Created the tree successfully using cli-smoke.jmx
Starting standalone test @ 2026 Oct 14 13:08:26 CEST (1791976106641)
Waiting for possible Shutdown/StopTestNow/HeapDump/ThreadDump message on port 4445
summary + 13 in 00:00:04 = 3.7/s Avg: 4 Min: 1 Max: 20 Err: 0 (0.00%) Active: 4 Started: 4 Finished: 0
summary + 97 in 00:00:10 = 9.9/s Avg: 3 Min: 1 Max: 7 Err: 0 (0.00%) Active: 5 Started: 5 Finished: 0
summary = 110 in 00:00:13 = 8.3/s Avg: 3 Min: 1 Max: 20 Err: 0 (0.00%)
summary + 96 in 00:00:10 = 9.6/s Avg: 3 Min: 1 Max: 8 Err: 0 (0.00%) Active: 5 Started: 5 Finished: 0
summary = 206 in 00:00:23 = 8.8/s Avg: 3 Min: 1 Max: 20 Err: 0 (0.00%)
summary + 100 in 00:00:10 = 9.8/s Avg: 3 Min: 1 Max: 11 Err: 0 (0.00%) Active: 5 Started: 5 Finished: 0
summary = 306 in 00:00:33 = 9.1/s Avg: 3 Min: 1 Max: 20 Err: 0 (0.00%)
summary + 64 in 00:00:06 = 10.3/s Avg: 2 Min: 1 Max: 8 Err: 0 (0.00%) Active: 0 Started: 5 Finished: 5
summary = 370 in 00:00:40 = 9.3/s Avg: 3 Min: 1 Max: 20 Err: 0 (0.00%)
Tidying up ... @ 2026 Oct 14 13:09:06 CEST (1791976146410)
... end of run
Source: Properties Reference: Summariser
The process exited with code 0 and left run1.jtl, run1.log and a report1 folder with index.html,
statistics.json, content/ and sbadmin2-1.0.7/. The console also started with a few warnings that are not
JMeter errors: Log4j StatusConsoleListener lines saying that package scanning for plugins is deprecated, and on macOS
a JVM warning about sun.awt.X11. Removing the Log4j package scanning is a planned 6.0 change.
Checking the numbers. Five threads, each waiting 500 ms after a request of about 3 ms, should produce
5 / 0.503 ≈ 9.9 requests per second, which is what the full-load intervals report. The first line covers only 4 s and
4 started threads because ramp-up starts one thread per second (5 threads over 5 s) and the summariser aligns its lines
to multiples of the interval on the wall clock, not to the start of the test. The last line has Active: 0 and
Finished: 5: the scheduler’s 40 s duration ended every thread.
The fields of a summariser line
| Field | Meaning |
|---|---|
|
The summariser’s name, from |
+ or |
Interval (delta) line or cumulative line. |
|
Number of samples and the elapsed time they cover, in HH:mm:ss (the format since 3.0). |
|
Throughput: samples per second over that period. |
|
Average, minimum and maximum elapsed time in milliseconds. There are no percentiles: use the dashboard for those. |
|
Failed samples and their percentage: failed requests and failed assertions alike. |
|
Threads currently running, started so far and finished so far. Only on |
By default (since 3.1) the summariser ignores the parent samples generated by Transaction Controllers, so its counts are real requests only.
Summariser properties
| Property | Default | Effect |
|---|---|---|
|
|
Name of the CLI summariser. An empty value ( |
|
30 |
Seconds between lines. Lines are aligned to multiples of the interval on the wall clock and are only printed when a sample completes, so a very slow plan can skip an interval. |
|
true |
Also write the lines to the run log, which keeps them after the console scroll-back is gone. |
|
true |
Write the lines to standard output. Set to |
|
true |
Ignore Transaction Controller parent samples in the counts. |
What to watch while a run is live. The summariser is the first warning system. A rate that stops rising while
Active keeps growing means the target (or the generator) is saturated; a rate that falls while Active is constant
means response times are growing, which in a closed model throttles the load by itself; a non-zero Err early in the
run usually means a data or correlation problem rather than load. The numbers are averages, so they are a trigger to
look at the dashboard or a live monitoring system, not a result.
Locale pitfall. The launcher sets -Duser.language=en -Duser.region=EN through JMETER_LANGUAGE. Starting
JMeter directly with java -jar ApacheJMeter.jar skips that, and on a machine with a comma decimal separator the
summariser then prints 1,7/s and (0,00%), which breaks scripts that parse these lines. Always use the launcher.
Stopping a run gracefully
A test ends by itself when every Thread Group has reached its loop count or duration. When it has to end earlier, there are two levels, matching the GUI’s Shutdown and Stop:
| Script (Unix / Windows) | Command sent | Effect | tearDown Thread Groups |
|---|---|---|---|
|
|
Graceful: threads finish their current sample and stop, no new iterations start. Results are flushed and the dashboard
( |
Run, if Run tearDown Thread Groups after shutdown of main threads is checked on the Test Plan |
|
|
Abrupt: samplers are interrupted. Results so far are flushed and the dashboard is still generated. |
Not run |
|
|
Writes a heap dump ( |
Not affected |
|
|
Writes a thread dump ( |
Not affected |
Source: Getting Started: Running JMeter (User Manual) and Building a Test Plan: Stop and Shutdown (User Manual)
How the port works
-
It is UDP, and local only. Each script runs
org.apache.jmeter.util.ShutdownClient, which sends the command word as a single UDP datagram tolocalhost. The JMeter process listens on a datagram socket and accepts commands only from a loopback address, so the scripts have to run on the same machine (or in the same container) as the test. Nothing on the network can stop the test, and nothing needs a firewall rule. -
The port is 4445 by default, then the next free one. At start-up JMeter tries
jmeterengine.nongui.port(4445) and, if it is busy, each port up tojmeterengine.nongui.maxport(4455). The console lineWaiting for possible Shutdown/StopTestNow/HeapDump/ThreadDump message on port 4446tells you which one it got. The scripts take the port as their only argument (shutdown.sh 4446) and default to 4445. -
Disabling it. A port of 1000 or less disables the listener. If every port in the range is busy, JMeter prints
Failed to create UDP portand the test runs without remote control. -
javamust be on the PATH. The.shscripts calljavadirectly, not$JAVA_HOME/bin/java.
| Property | Default | Effect |
|---|---|---|
|
4445 |
First UDP port tried for shutdown commands in CLI mode. 1000 or less disables the listener. |
|
4455 |
Last port tried if the first is busy. A value less than or equal to the |
|
5000 |
Milliseconds to wait for a thread to stop. |
|
true |
In CLI mode, call |
|
false |
Call |
|
0 (see note) |
Milliseconds after the end of the run before a daemon thread reports non-daemon threads that keep the JVM alive. |
Source: Properties Reference (JMeter engine section)
The documentation disagrees with itself on two of these defaults. Getting Started says jmeterengine.stopfail.system.exit
defaults to false, while the properties reference, the commented line in jmeter.properties and the 5.6.3 source all
use true. For jmeter.exit.check.pause the properties reference gives 2000, while Getting Started, jmeter.properties
and the source use 0 (no check). The values in the table are the ones the 5.6.3 code uses.
A real shutdown and stop
Two CLI instances were started on the same machine against the same local target; the second one found 4445 busy and
took 4446. shutdown.sh then stopped the first gracefully and stoptest.sh 4446 stopped the second abruptly
(genuine 5.6.3 output, summariser interval 10 s):
jmeter -n -t cli-smoke.jmx -l a.jtl -f -Jthreads=5 -Jrampup=5 -Jduration=300 -Jsummariser.interval=10 &
jmeter -n -t cli-smoke.jmx -l b.jtl -f -Jthreads=2 -Jduration=300 -Jsummariser.interval=10 &
$JMETER_HOME/bin/shutdown.sh
$JMETER_HOME/bin/stoptest.sh 4446
Sending Shutdown request to port 4445
Sending StopTestNow request to port 4446
The first instance:
Waiting for possible Shutdown/StopTestNow/HeapDump/ThreadDump message on port 4445
summary + 1 in 00:00:01 = 1.6/s Avg: 25 Min: 25 Max: 25 Err: 0 (0.00%) Active: 1 Started: 1 Finished: 0
summary + 55 in 00:00:07 = 7.4/s Avg: 3 Min: 1 Max: 9 Err: 0 (0.00%) Active: 5 Started: 5 Finished: 0
summary = 56 in 00:00:08 = 6.9/s Avg: 3 Min: 1 Max: 25 Err: 0 (0.00%)
Command: Shutdown received from /127.0.0.1
summary + 89 in 00:00:09 = 9.6/s Avg: 2 Min: 0 Max: 10 Err: 0 (0.00%) Active: 0 Started: 5 Finished: 5
summary = 145 in 00:00:17 = 8.3/s Avg: 3 Min: 0 Max: 25 Err: 0 (0.00%)
Tidying up ... @ 2026 Oct 14 13:10:09 CEST (1791976209300)
... end of run
The second instance:
Waiting for possible Shutdown/StopTestNow/HeapDump/ThreadDump message on port 4446
summary + 14 in 00:00:04 = 3.4/s Avg: 5 Min: 3 Max: 23 Err: 0 (0.00%) Active: 2 Started: 2 Finished: 0
Command: StopTestNow received from /127.0.0.1
summary + 38 in 00:00:10 = 3.9/s Avg: 5 Min: 1 Max: 84 Err: 0 (0.00%) Active: 0 Started: 2 Finished: 2
summary = 52 in 00:00:14 = 3.8/s Avg: 5 Min: 1 Max: 84 Err: 0 (0.00%)
Tidying up ... @ 2026 Oct 14 13:10:09 CEST (1791976209916)
... end of run
Both processes exited with code 0. When several instances share one generator, give each a fixed port so that the operator does not have to read the console to find it:
jmeter -n -t bookshelf-load.jmx -l search.jtl -Jjmeterengine.nongui.port=4501 -Jjmeterengine.nongui.maxport=4501 &
jmeter -n -t bookshelf-load.jmx -l loans.jtl -Jjmeterengine.nongui.port=4502 -Jjmeterengine.nongui.maxport=4502 &
$JMETER_HOME/bin/shutdown.sh 4502 # stop only the second one
In containers. The command has to originate inside the container’s network namespace, so run the script there:
docker exec <container> /opt/apache-jmeter-5.6.3/bin/shutdown.sh (or kubectl exec for a pod), with the path of the
image’s JMeter installation. A docker stop instead sends SIGTERM, which is covered next.
Ctrl+C, SIGTERM and kill
JMeter registers a JVM shutdown hook that flushes and closes the results files, so Ctrl+C (SIGINT), kill (SIGTERM)
and docker stop keep the samples recorded so far. They do not run the graceful shutdown: in-flight samples are lost,
tearDown Thread Groups do not run, and the -e dashboard is not generated. Measured on 5.6.3: a run killed with
SIGTERM after 8 s exited with code 143 (the shell convention for 128 plus signal 15) and left a complete, readable JTL
file and no report folder. Rebuild the report with jmeter -g results.jtl -o report. SIGKILL (kill -9) runs no hook
at all, and the last buffered samples can be lost.
Order of preference: shutdown.sh, then stoptest.sh if threads are stuck in long timeouts, then SIGTERM, and
SIGKILL only if the JVM does not react.
Source: Changes History (2.10: shutdown hook for kill and Ctrl+C) and Getting Started: CLI mode shutdown (User Manual)
When the JVM does not exit
Since 2.5.1 JMeter no longer calls System.exit() when a CLI test ends normally; it ends its own non-daemon threads
and lets the JVM exit. A plugin or script that leaves a non-daemon thread running keeps the JVM alive after
... end of run, and a CI job then hangs. Two properties address this: jmeter.exit.check.pause (for example
-Jjmeter.exit.check.pause=2000) makes JMeter print the threads that are still alive after that pause, which names the
culprit; jmeterengine.force.system.exit=true forces the exit. Fix the plugin or script where possible and use the
second only as a workaround.
Exit codes
The manual does not document exit codes for CLI runs beyond the two System.exit properties above, so the table below
was measured on a 5.6.3 installation and cross-checked against the 5.6.3 source (JMeter.java,
StandardJMeterEngine.java).
| Situation | Exit code | Where it comes from |
|---|---|---|
Test completed normally, even with 100 % failed samples |
0 |
Normal end of the JVM; failures only show in the JTL, summariser and dashboard |
Stopped with |
0 |
Normal end after the stop |
|
0 |
Normal end |
Plan file missing, plan cannot be parsed (for example a function in an |
1 |
Caught in |
Explicit stop requested but threads cannot be stopped ( |
1 |
Prints |
Usage errors such as |
0 |
Prints |
Process killed with SIGTERM or Ctrl+C |
143 / 130 |
Shell convention (128 plus the signal number), not a JMeter value |
|
0 |
Forced |
Source: JMeter.java (5.6.3 source) and Properties Reference
Two consequences for automation:
-
Exit code 0 does not mean the test passed. A run where every request failed returns 0. In the measurement above, a run aimed at a closed port finished with
Err: 17 (100.00%)and exit code 0. -
Exit code 0 does not even mean the test ran. A mistyped option returns 0 after printing the usage text. Check that the results file exists and is not empty.
Gating a build on the results
The dashboard writes statistics.json, whose Total entry holds the overall error percentage and response-time
percentiles (pct1ResTime, pct2ResTime and pct3ResTime are the 90th, 95th and 99th percentiles by default). A
gate after the run turns them into an exit code. The thresholds below are the Bookshelf search SLA: under 1 % errors
and a 95th percentile under 800 ms.
#!/usr/bin/env bash
set -euo pipefail
jmeter -n -t bookshelf-load.jmx -q profiles/load.properties \
-l results/load.jtl -j results/load.log -f -e -o results/report
# the run itself may "succeed" with no data: check for samples first
test -s results/load.jtl || { echo "no results written"; exit 2; }
# jq -e exits with 1 when the expression is false
jq -e '.Total.errorPct < 1 and .Total.pct2ResTime < 800' results/report/statistics.json \
|| { echo "SLA failed"; jq '.Total' results/report/statistics.json; exit 1; }
The jmeter-maven-plugin’s results goal does a similar check inside a Maven build; it and GitHub Actions are covered in
the page on CI/CD with Maven, Docker and GitHub Actions.
Load-generator sizing
A load test measures the system under test only while the load generator is not itself the bottleneck. If the generator runs out of CPU, heap, sockets or bandwidth, response times grow and throughput falls in exactly the way a slow server would look. The best-practices chapter puts it in terms of coordinated omission: a generator that cannot send requests on schedule silently omits the slow periods, and the results look better or worse than reality. The rule of this section is therefore: size the generator, monitor it during every run, and keep its metrics with the report.
The thread-count calibration loop (step up -Jthreads until the rate stops rising) is on
Thread Groups: Concurrent Users & Ramp-up. This
section covers the resources one by one.
Heap and memory
The launchers set the heap through the HEAP variable, -Xms1g -Xmx1g -XX:MaxMetaspaceSize=256m by default, and always
add -XX:+HeapDumpOnOutOfMemoryError, so an out-of-memory error leaves a .hprof file to analyse.
# one run with a 4 GiB heap
HEAP="-Xms4g -Xmx4g -XX:MaxMetaspaceSize=256m" \
jmeter -n -t bookshelf-load.jmx -q profiles/load.properties -l load.jtl -f
What uses the heap in a CLI run:
-
Sample results in flight. Each sample holds its response body until listeners, extractors and assertions have seen it. Large responses (downloads, big JSON) times many threads is the main cost.
-
Listeners that keep data. View Results Tree, View Results in Table, Aggregate Report and graph listeners keep results in memory even in CLI mode. Remove or disable them;
-lalready writes everything. -
Test data loaded into memory. CSV files are read line by line, but scripts that load whole files into variables or properties keep them for the run.
-
Compiled scripts and per-thread state. JSR223 compiled-script caches, cookie and cache managers per thread.
What uses memory outside the heap, and therefore outside HEAP: metaspace (bounded by MaxMetaspaceSize), thread
stacks, direct buffers and the JVM itself. Thread stacks are the one that grows with load: the Java 21 java reference
gives a default stack size (-Xss) of 1024 KB on Linux/x64 and macOS/x64 and 2048 KB on Linux/AArch64 and
macOS/AArch64. That is reserved address space, committed only as the stack is used, but it sets the ceiling.
Worked example. 500 threads on a Linux/x64 generator reserve up to 500 × 1 MB ≈ 0.5 GB of stack outside the heap.
With -Xmx4g and 256 MB of metaspace, plan for about 5 GB of process memory plus the operating system and any agent on
the machine, so an 8 GB machine is the minimum and the heap should not be raised further without more RAM: a
generator that swaps measures its own disk. Keep -Xms equal to -Xmx so the heap does not resize during the test.
In containers, modern JDKs size the default heap from the container memory limit (-XX:MaxRAMPercentage, default
25 %). The JMeter launcher sets an explicit -Xmx through HEAP, which takes precedence, so set HEAP to fit the
container limit (for example 75 % of it). The commented-out RUN_IN_DOCKER line in bin/jmeter uses
-XX:+UseCGroupMemoryLimitForHeap, an experimental option for Java 8u131 and 9 that current JDKs no longer
accept; leave it commented out.
Source: The java Command (Java SE 21)
Garbage collection
The launchers select G1 through GC_ALGO. The shipped 5.6.3 scripts (bin/jmeter and bin/jmeter.bat) use
-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:G1ReservePercent=20; the Getting Started page lists the same
options with MaxGCPauseMillis=250. The script is what actually runs. Either way, a GC pause stops every thread of the
generator, and a pause in the middle of a sample is added to that sample’s response time.
To see whether GC is a problem, enable a GC log for the run. bin/jmeter contains a ready-made VERBOSE_GC line for
Java 9 and later; the same thing through JVM_ARGS:
JVM_ARGS="-Xlog:gc*:file=gc_jmeter_%p.log:time,uptime" \
jmeter -n -t bookshelf-load.jmx -q profiles/load.properties -l load.jtl -f
or watch a running generator with the JDK tools:
jcmd | grep ApacheJMeter # find the process id
jstat -gcutil <pid> 5s # heap occupancy and GC counts every 5 s
Healthy: young collections of a few milliseconds, old generation that drops back after collections, no full GC. Undersized: old generation climbing towards 100 %, back-to-back collections, full GCs, rising response times on every sampler at once. The fix is more heap or less retained data, not a different collector.
CPU
CPU cost grows with requests per second and with the work per response, not with the number of threads: a thread sleeping in a timer costs almost nothing. The expensive parts are TLS handshakes (especially without keep-alive), compression, extractors and assertions on large bodies (regular expressions and XPath are the costly ones), JSR223 scripts that are not cached, and writing results. Keep the generator’s CPU well below saturation during the hold; a common rule of thumb is to stay under about 80 %, the utilisation figure Pro Apache JMeter (ch. 2) works with. Above that, scheduling delays inflate response times and the summariser’s rate flattens while the server is idle.
To find the ceiling of a machine without a network, the best-practices chapter suggests the Java Request sampler with
its JavaTest class, which needs no network access and shows the maximum throughput achievable on that platform.
Network
The generator’s network link carries every request and every response. Worked example for the Bookshelf mix: at
150 requests per second with an average response of 20 KB, the generator receives 150 × 20 KB = 3 MB/s, about
24 Mbit/s, comfortable on a 1 Gbit/s link. A catalogue export of 2 MB requested 50 times a second is 100 MB/s, 800 Mbit/s,
and saturates the same link before the server. The JTL’s bytes and sentBytes columns and the dashboard’s
receivedKBytesPerSec give the real figure after a smoke run; compare it with the link capacity.
Also check:
-
Latency to the target. A generator in another region adds its round-trip time to every sample and changes the number of threads needed for a target rate (Little’s Law). Put generators close to the users being modelled, or close to the system when measuring the system alone.
-
Name resolution. The DNS Cache Manager or a hosts file avoids measuring a slow resolver.
-
Bandwidth throttling with
httpclient.socket.http.cpssimulates slow clients but does not reduce the generator’s own work.
File descriptors
Every open socket is a file descriptor, as are the results files, the run log and every jar on JMeter’s classpath. A
thread with keep-alive holds one connection per target host; with embedded resources and parallel downloads it can
hold up to the pool size (default 6) more. The per-process limit is the shell’s ulimit -n, which on many Linux
distributions is 1024 for interactive sessions.
Worked example. 500 threads with keep-alive to one host: about 500 sockets, plus roughly 150 for jars, logs and
results, already over half of 1024. With parallel downloads of embedded resources (up to 6 per thread) the need rises
to about 3,500 and the run fails with java.net.SocketException: Too many open files. Raise the limit before the run:
ulimit -n # current soft limit
ulimit -n 65536 # raise it for this shell (up to the hard limit, ulimit -Hn)
jmeter -n -t bookshelf-load.jmx -q profiles/load.properties -l load.jtl -f
For services started by systemd, the limit is LimitNOFILE= in the unit; for containers, the runtime’s --ulimit nofile=
option. macOS uses ulimit -n and kern.maxfilesperproc in the same way.
Ephemeral ports
Every outgoing TCP connection uses a local (ephemeral) port. When a connection is closed by the client, the socket
stays in TIME_WAIT for 60 seconds on Linux (the kernel constant TCP_TIMEWAIT_LEN) before its port can be reused for
the same destination. The Linux range is net.ipv4.ip_local_port_range, by default 32768 to 60999, which is 28,232
ports per destination address and port; Windows uses 49152 to 65535 (16,384 ports) by default.
Worked example. A plan that opens a new connection per request (keep-alive off, or a fresh user on each iteration
that makes one request) at 500 requests per second leaves 500 × 60 = 30,000 sockets in TIME_WAIT, more than the 28,232
ports available: after about a minute connections fail with java.net.BindException: Cannot assign requested address
(Linux) or Address already in use: connect (Windows), and the errors look like server failures. The Bookshelf load
profile, by contrast, keeps one connection per thread: 50 connections for the whole run.
Ways to stay clear, in order of preference:
-
Reuse connections. Keep Use KeepAlive on in HTTP Requests and keep Same user on each iteration checked on the Thread Group when the scenario allows it; with
httpclient.reset_state_on_thread_group_iteration(defaulttrue) a new user closes its connections at each iteration.httpclient4.time_to_live(default 60000 ms) bounds how long a connection is reused. -
Spread the load over more generators, or over several source addresses with the HTTP Request’s source address field, since the limit applies per source address, destination address and port.
-
Widen the range on the generator (
sysctl -w net.ipv4.ip_local_port_range="15000 64000"on Linux, ornetsh int ipv4 set dynamic tcp start=10000 num=50000on Windows). -
TIME_WAIT reuse.
net.ipv4.tcp_tw_reuse(default 2, enabled for loopback only) can be set to 1, but the kernel documentation says it should not be changed without advice from technical experts.
Count TIME_WAIT sockets during a run with ss -tan state time-wait | wc -l on Linux.
Monitoring the generator itself
Record the generator’s own metrics for the duration of every run, next to the server’s, and keep them with the report. A test whose generator was saturated is not a valid result, whatever the dashboard says.
| Resource | What to watch | Tools |
|---|---|---|
CPU |
Total and per-core utilisation, run-queue length; stay under about 80 % |
|
Heap and GC |
Old-generation occupancy, GC pause times and frequency, full GCs |
GC log ( |
Memory |
Resident size of the JVM against physical RAM, swap activity |
|
Network |
Throughput against link capacity, retransmissions, TIME_WAIT count |
|
File descriptors |
Open count against |
|
JMeter |
|
Summariser lines, run log, Backend Listener |
Real-time dashboards for the JMeter side (Backend Listener to InfluxDB or Graphite, viewed in Grafana) are the subject of Real-time Monitoring with Backend Listener; the generator’s operating-system metrics can go to the same Prometheus and Grafana as the server’s, see Grafana: Dashboards & Visualization. For CPU and memory limits of generators running in Kubernetes see Resource Management.
Sizing worked example
Target: the Bookshelf journey at 150 requests per second, one generator, Linux/x64.
| Resource | Reasoning |
|---|---|
Threads |
Each user waits 3 s and a request takes about 0.2 s: 3.2 s per request per thread, so 150 × 3.2 = 480 threads (Little’s Law). Run 500. |
Heap |
Start at 2 GiB with |
Process memory |
2 GiB heap + 256 MB metaspace + up to 0.5 GB of thread stacks + JVM overhead: about 3.5 GB. A 4-vCPU, 8 GB machine. |
CPU |
150 requests per second of small JSON with keep-alive is light; confirm that CPU stays under about 80 % in the calibration run. |
Network |
150 × 20 KB ≈ 3 MB/s received: no concern on 1 Gbit/s. |
File descriptors |
About 500 sockets plus about 150 other files: raise |
Ephemeral ports |
Keep-alive and Same user on each iteration: about 500 long-lived connections and almost no TIME_WAIT. Without them, one new connection per iteration (about 16 s per iteration) is 500 / 16 ≈ 31 per second, about 1,900 in TIME_WAIT: still safe. |
If any line does not fit one machine, use two generators of half the load each and merge the results files, or run in distributed mode.
Pitfalls
-
Running the load from the GUI "just this once". The numbers are not comparable with CLI runs.
-
GUI listeners left in the plan. They still cost memory and CPU in CLI mode. Disable them; rely on
-l. -
Appending to an old JTL. Without
-for unique names, a dashboard mixes runs. -
Trusting the exit code. 0 after 100 % errors, and 0 after a mistyped option. Gate on the results.
-
java -jar ApacheJMeter.jarinstead of the launcher: noHEAP, noGC_ALGO, noJMETER_LANGUAGE, so a 25 % default heap and locale-dependent summariser numbers. -
Killing instead of shutting down. SIGTERM keeps the JTL but skips the dashboard and tearDown; use
shutdown.sh. -
Running
shutdown.shon the wrong machine or outside the container. The command is loopback-only UDP and is silently ignored anywhere else; the script itself reports success because UDP has no acknowledgement. -
Port confusion with several instances. The second instance is on 4446;
shutdown.shwithout an argument stops the first. -
Proxy credentials on the command line are visible in the process list. Use a system properties file with
-S. -
Debug logging in a load run turns the disk into the bottleneck.
-
An undersized generator looks like a slow server: check CPU, GC, file descriptors and TIME_WAIT before blaming the application.
From the books to 5.6 to 6.0
| Topic | Books (JMeter 2.11 to 3.0) | 5.6.3, and planned 6.0 |
|---|---|---|
Name |
"Non-GUI mode" (Pro ch. 3) |
"CLI mode" in the manual since 5.1; the option is still |
Dashboard |
Introduced in 3.0, |
Same options; |
Summariser |
Enabled by default in CLI mode since 2.11, durations in HH:mm:ss since 3.0 |
Ignores Transaction Controller samples since 3.1; same properties |
Shutdown |
UDP 4445 to 4455 since 2.5, shutdown hook for kill and Ctrl+C since 2.10 |
Unchanged; |
Heap |
|
|
Java |
Java 7 or 8 |
Java 8+ for 5.6.3 (17+ recommended); 6.0 plans to require Java 17 (21 recommended) |
Start-up warnings |
None |
Log4j "package scanning is deprecated" lines on 5.6.3; planned removal in 6.0 |
Numeric properties |
Parsed as written |
6.0 plans to trim whitespace when parsing numeric properties |
The Spring Boot-specific wiring (Actuator metrics, the Maven plugin) is on Performance Testing with JMeter; running JMeter in a GitHub Actions job builds on Docker CI/CD with GitHub Actions.
References
-
Getting Started (User Manual): sections 1.4.3 (proxy), 1.4.4 (CLI mode), 1.4.5 (server mode), 1.4.6 (overriding properties), 1.4.7 (logging), 1.4.8 (full list of command-line options), 1.4.9 (CLI mode shutdown) and 1.5 (configuring JMeter)
-
Properties Reference: summariser, JMeter engine, HttpClient4 and dashboard sections
-
Download Apache JMeter (5.6.3 binaries and SHA-512 checksums)
-
Apache JMeter source code: tag
rel/v5.6.3(bin/jmeter,bin/jmeter.bat,bin/shutdown.sh,JMeter.java,StandardJMeterEngine.java,Summariser.java,ShutdownClient.java,ResultCollector.java) for the launcher defaults, the UDP listener, the summariser timing and the exit codes;xdocs/changes.xmlon master for the planned 6.0 changes -
The java Command (Java SE 21):
-Xss,-XX:MaxRAMPercentage,-Xlog -
IP Sysctl (Linux kernel documentation):
ip_local_port_range,tcp_tw_reuse -
Linux include/net/tcp.h (
TCP_TIMEWAIT_LEN) -
The default dynamic port range for TCP/IP has changed (Microsoft Learn)
-
Sai Matam and Jagdeep Jain, Pro Apache JMeter, Apress, 2017 (JMeter 3.0): ch. 2 (resource utilisation targets), ch. 3 (non-GUI mode), ch. 8 (troubleshooting and heap), ch. 12 (dashboard); used for concepts only, with every command taken from the current manual.