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

shutdown.sh and stoptest.sh send commands to a UDP port (4445 by default)

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.

sequenceDiagram participant Sh as Shell or CI job participant L as bin/jmeter launcher participant E as JMeter engine participant U as UDP listener (port 4445) participant F as Files Sh->>L: jmeter -n -t plan.jmx -l results.jtl -e -o report L->>E: java with HEAP, GC_ALGO, JVM_ARGS E->>E: load properties, then -q files and -J values in order E->>F: load plan.jmx (missing or invalid file ends with exit code 1) E->>U: bind 4445, or the next free port up to 4455 loop every summariser.interval seconds E-->>Sh: summary + and summary = lines E->>F: append samples to results.jtl end Sh->>U: shutdown.sh sends Shutdown (optional) U->>E: stop after current samples E->>F: flush results.jtl E->>F: generate dashboard into report E-->>Sh: Tidying up, end of run, exit code 0

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
  • -n runs without the GUI.

  • -t names the plan.

  • -l names the sample results file (JTL). Its format comes from jmeter.save.saveservice.output_format (CSV by default), not from the file extension.

  • -j names the JMeter run log, which is otherwise jmeter.log in the current directory.

  • -J sets JMeter properties that the plan reads with ${__P(threads,1)} and friends.

  • -f deletes an existing results file and report folder before starting.

  • -e generates 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).

-h

--help

Print usage information and exit.

-v

--version

Print the version banner and exit. Useful at the start of every CI job to record the exact version in the log.

-p

--propfile

file

Use this file instead of bin/jmeter.properties. Rarely what you want: a copy of jmeter.properties goes stale on the next upgrade. Prefer -q or user.properties.

-q

--addprop

file

Load an additional JMeter properties file. May be repeated. A missing file is only a warning in the run log.

-t

--testfile

file

The .jmx plan to run. -t LAST loads the last plan used in GUI mode.

-l

--logfile

file

The sample results file (JTL). If the file exists and -f is not given, new samples are appended.

-i

--jmeterlogconf

file

The Log4j 2 configuration file to use instead of bin/log4j2.xml.

-j

--jmeterlogfile

file

The JMeter run log. Default jmeter.log in the current directory. A name with a date pattern in single quotes is formatted with the current date (see below).

-n

--nongui

Run in CLI mode. Without it, JMeter opens the GUI (and loads the -t plan into it).

-s

--server

Run as a JMeter server (the jmeter-server script does this) for distributed testing.

-E

--proxyScheme

scheme

Scheme of the outbound proxy, for non-HTTP proxies (optional).

-H

--proxyHost

host

Outbound proxy host. Sets http.proxyHost and https.proxyHost.

-P

--proxyPort

port

Outbound proxy port. Sets http.proxyPort and https.proxyPort.

-N

--nonProxyHosts

host list

Hosts that bypass the proxy, separated by a vertical bar. Sets http.nonProxyHosts and https.nonProxyHosts.

-u

--username

user

User name for proxy authentication.

-a

--password

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 (-S).

-J

--jmeterproperty

name=value

Define a JMeter property, read in the plan with __P or __property. -Jname= with an empty value removes the property.

-G

--globalproperty

name=value or file

Define a JMeter property that is sent to every remote server, or (without =) a properties file whose contents are sent. Only meaningful with -r or -R.

-D

--systemproperty

name=value

Define a Java system property, for example -Djavax.net.ssl.keyStore=.... -Dname= with an empty value removes it.

-S

--systemPropertyFile

file

Load an additional system properties file. May be repeated.

-f

--forceDeleteResultFile

Delete existing results files and the -o report folder before the test starts. It deletes the file of every listener in the plan that writes to a file, not only the -l file.

-L

--loglevel

[category=]level

Override a log level, for example -Ljmeter.engine=DEBUG or -LDEBUG for the root logger. Categories starting with jmeter or jorphan get org.apache. prepended.

-r

--runremote

Start the remote servers listed in the remote_hosts property.

-R

--remotestart

host list

Start these remote servers (comma separated), overriding remote_hosts without redefining the property.

-d

--homedir

directory

The JMeter home directory to use.

-X

--remoteexit

Exit the remote servers at the end of the test (CLI mode).

-g

--reportonly

results file

Generate the dashboard from an existing results file, without running a test. Needs -o.

-e

--reportatendofloadtests

Generate the dashboard after the load test. Needs -l and -o.

-o

--reportoutputfolder

folder

Output folder of the dashboard. It must not exist or must be empty, unless -f is given.

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 -l file and no -f appends the second run to the first, without a second header line; a dashboard generated from that file then mixes two runs. Either pass -f or give every run its own file name.

  • The report folder must be empty. With -e -o report and a non-empty report, 5.6.3 stops before the test starts with Cannot write to '...' as folder is not empty and exit code 1. -f empties it first.

  • -e needs -l. Without a results file there is nothing to build the dashboard from; JMeter prints Incorrect Usage:Option -e requires -l option and the usage text.

  • Date patterns in the run log name. If the -j name 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 LAST and -l LAST refer 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

threads

${__P(threads,1)}

1

Number of threads (virtual users)

rampup

${__P(rampup,1)}

1

Ramp-up period in seconds

duration

${__P(duration,60)}

60

Scheduler duration in seconds; it includes the ramp-up

host, port

${__P(host,localhost)}, ${__P(port,8080)}

localhost, 8080

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 counters Active, Started and Finished;

  • 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

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

summary

The summariser’s name, from summariser.name. Several Generate Summary Results listeners with the same name add to the same totals.

+ or =

Interval (delta) line or cumulative line.

370 in 00:00:40

Number of samples and the elapsed time they cover, in HH:mm:ss (the format since 3.0).

= 9.3/s

Throughput: samples per second over that period.

Avg, Min, Max

Average, minimum and maximum elapsed time in milliseconds. There are no percentiles: use the dashboard for those.

Err: 0 (0.00%)

Failed samples and their percentage: failed requests and failed assertions alike.

Active, Started, Finished

Threads currently running, started so far and finished so far. Only on + lines.

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

summariser.name

summary

Name of the CLI summariser. An empty value (-Jsummariser.name=) disables it; the run then prints no summary lines at all.

summariser.interval

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.

summariser.log

true

Also write the lines to the run log, which keeps them after the console scroll-back is gone.

summariser.out

true

Write the lines to standard output. Set to false in CI jobs that only need the log file.

summariser.ignore_transaction_controller_sample_result

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

shutdown.sh / shutdown.cmd

Shutdown

Graceful: threads finish their current sample and stop, no new iterations start. Results are flushed and the dashboard (-e) is generated.

Run, if Run tearDown Thread Groups after shutdown of main threads is checked on the Test Plan

stoptest.sh / stoptest.cmd

StopTestNow

Abrupt: samplers are interrupted. Results so far are flushed and the dashboard is still generated.

Not run

heapdump.sh / heapdump.cmd

HeapDump

Writes a heap dump (dump_<timestamp>.hprof) into the current directory of the JMeter process. The test continues.

Not affected

threaddump.sh / threaddump.cmd

ThreadDump

Writes a thread dump (thread_dump_<timestamp>.log) into the current directory of the JMeter process. The test continues.

Not affected

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 to localhost. 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 to jmeterengine.nongui.maxport (4455). The console line Waiting for possible Shutdown/StopTestNow/HeapDump/ThreadDump message on port 4446 tells 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 port and the test runs without remote control.

  • java must be on the PATH. The .sh scripts call java directly, not $JAVA_HOME/bin/java.

Property Default Effect

jmeterengine.nongui.port

4445

First UDP port tried for shutdown commands in CLI mode. 1000 or less disables the listener.

jmeterengine.nongui.maxport

4455

Last port tried if the first is busy. A value less than or equal to the .port property disables the search.

jmeterengine.threadstop.wait

5000

Milliseconds to wait for a thread to stop.

jmeterengine.stopfail.system.exit

true

In CLI mode, call System.exit(1) if threads cannot be stopped after an explicit stop request.

jmeterengine.force.system.exit

false

Call System.exit(0) at the end of every CLI test, even a normal one, for plans whose plugins leave non-daemon threads behind.

jmeter.exit.check.pause

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 shutdown.sh or stoptest.sh

0

Normal end after the stop

-v, -h, -?, report-only -g that succeeds

0

Normal end

Plan file missing, plan cannot be parsed (for example a function in an intProp), -o folder not empty, unknown positional argument

1

Caught in JMeter.start(): prints An error occurred: ... and calls System.exit(1)

Explicit stop requested but threads cannot be stopped (jmeterengine.stopfail.system.exit=true)

1

Prints Fatal error, could not stop test, exiting

Usage errors such as -e without -l, or an unknown option such as -Z

0

Prints Incorrect Usage:... or Error: Unknown option plus the usage text and returns normally

Process killed with SIGTERM or Ctrl+C

143 / 130

Shell convention (128 plus the signal number), not a JMeter value

jmeterengine.force.system.exit=true at the end of a test

0

Forced System.exit(0)

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; -l already 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.

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.cps simulates 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:

  1. 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 (default true) a new user closes its connections at each iteration. httpclient4.time_to_live (default 60000 ms) bounds how long a connection is reused.

  2. 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.

  3. Widen the range on the generator (sysctl -w net.ipv4.ip_local_port_range="15000 64000" on Linux, or netsh int ipv4 set dynamic tcp start=10000 num=50000 on Windows).

  4. 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 %

top, vmstat 5, mpstat, Task Manager, node exporter in Prometheus

Heap and GC

Old-generation occupancy, GC pause times and frequency, full GCs

GC log (-Xlog:gc), jstat -gcutil, jcmd, JDK Flight Recorder

Memory

Resident size of the JVM against physical RAM, swap activity

free, vmstat, ps -o rss

Network

Throughput against link capacity, retransmissions, TIME_WAIT count

sar -n DEV, ss -s, ss -tan state time-wait

File descriptors

Open count against ulimit -n

ls /proc/<pid>/fd | wc -l, lsof -p <pid>

JMeter

Active threads against the plan, rate against the expected rate, errors

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 -Xms2g -Xmx2g, run a 10-minute calibration and read the GC log. If the old generation does not settle below about 60 % after collections, double it.

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 ulimit -n to 65536 anyway.

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 -f or 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.jar instead of the launcher: no HEAP, no GC_ALGO, no JMETER_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.sh on 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.sh without 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 -n / --nongui

Dashboard

Introduced in 3.0, -e -o and -g (Pro ch. 12)

Same options; -f also empties the -o folder since 5.0 and works with -g since 5.1

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; HeapDump and ThreadDump commands and their scripts are available too

Heap

HEAP edited in the launcher script (Pro ch. 8)

HEAP defaults to 1 GiB and is set through the environment or setenv.sh; heap dump on OOM is always on

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