Distributed Testing

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.

A single JMeter instance on a decent machine can drive a lot of traffic, but not an unlimited amount. When the load generator’s CPU, memory or network becomes the bottleneck, the numbers measure the generator rather than the system under test. Distributed testing spreads the load over several machines. JMeter’s built-in way to do that is remote testing: one controller sends the test plan over Java RMI to several workers running jmeter-server. Each worker runs the plan against the target and streams its sample results back to the controller, which writes the results file. This page covers the architecture, a complete setup, RMI security and networking, how results travel back, failed workers, and the alternatives that avoid RMI altogether.

It was written against Apache JMeter 5.6.3, the latest release at the time of writing, on Java 21 (5.6.x runs on Java 8 or newer; this guide recommends 17 or newer, and the planned 6.0 needs 17). Every property was checked against the properties reference, the jmeter.properties file shipped with 5.6.3 and the 5.6.3 source code. The command output shown on this page comes from a real 5.6.3 installation: two workers and a controller on one machine, using loopback addresses and non-default ports so they could coexist. On real networks, use one machine per worker. JMeter 6.0 is not released, and the planned changes in changes.xml on the development branch do not alter remote testing beyond the Java 17 minimum. The books call the two roles master and slave and describe distributed testing without SSL (Pro Apache JMeter ch. 6, JMeter 3.0; JMeter Cookbook ch. 5, JMeter 2.11). The current manual uses controller and worker, or client and server, and RMI over SSL has been the default since 4.0.

This page builds on Running Tests in CLI Mode (the -r, -R, -G and -X options in their context) and Properties, Variables & Functions (why -J does not reach the workers). The results file and the dashboard it produces are covered in Listeners & Results Files and HTML Dashboard Report.

When one generator is not enough

Distribute a test only after the plan is lean and the single generator has been tuned (CLI mode, few listeners, CSV results, enough heap). Distribution multiplies whatever inefficiency the plan has. The signs that a generator has reached its limit are the same as on any other machine:

  • CPU of the JMeter process above roughly 80 % for long periods, so timers fire late and response times include time spent waiting for a CPU;

  • garbage collection pauses visible in the GC log, or heap close to -Xmx;

  • the network interface close to its bandwidth, or many sockets in TIME_WAIT;

  • throughput that stops growing when threads are added while the server under test is still idle.

The step-by-step tutorial in the manual gives a rough figure: a single JMeter client on a recent 2-3 GHz CPU can handle 1,000 to 2,000 threads, depending on the type of test. The figure is old and depends heavily on think time, response size and the elements in the plan. Measure your own generator rather than relying on it.

Worked example. The Bookshelf load profile uses 50 threads, a 3 s think time and a typical 0.2 s response time. By Little’s Law each thread does 1 / (3 + 0.2) ≈ 0.31 requests per second, so 50 threads produce about 15.6 req/s. One generator handles that easily. A target of 2,000 concurrent users with the same think time means 2,000 / 3.2 ≈ 625 req/s. If measurement shows that one generator stays healthy up to 500 threads for this plan, four workers with -Gthreads=500 are needed, and a fifth gives headroom for a failed node.

Architecture: the whole plan runs on every worker

JMeter distributed topology: the controller sends the plan over RMI with TLS to N workers running jmeter-server, every worker runs the whole plan against the Bookshelf API, and sample results flow back to the controller

The manual defines three roles:

Role What it does How it is started

Controller (client)

Loads the plan, sends it to every worker, starts and stops the test, receives the sample results and writes the results file, the summariser lines and the dashboard. It generates no load itself.

jmeter -n -t plan.jmx -r or -R host1,host2 (or the GUI’s Run → Remote Start)

Worker (server)

Runs the plan it receives, exactly as a local CLI run would, and sends every sample back to the controller.

bin/jmeter-server (or jmeter -s)

Target

The system under test; it only sees HTTP (or JDBC, JMS…​) traffic from the workers.

Your own deployment, here the Bookshelf API

The most important rule is in the manual’s own words: JMeter does not distribute the load between servers, and each one runs the full test plan. If the Thread Group says 1,000 threads and there are 6 servers, 6,000 threads are injected. To reproduce the 50-thread Bookshelf profile with two workers, give each worker 25 threads:

Setup threads per worker Workers Total concurrent users

Single machine, -Jthreads=50

50

1

50

Distributed, -Gthreads=25

25

2

50

Distributed, -Gthreads=50 (common mistake)

50

2

100

Everything else in the plan multiplies in the same way. A Constant Throughput Timer or Precise Throughput Timer target applies per worker, so the total throughput target is the per-worker value times the number of workers. The same is true for an Open Model Thread Group schedule (rate(10/sec) on four workers is 40 requests per second in total), for a Throughput Shaping Timer and for an Arrivals Thread Group. A ramp-up of 60 s means every worker ramps its own threads over 60 s at the same time, so the total ramp rate is the per-worker rate times the number of workers. Load-shaping plugins and the Open Model Thread Group are covered in Open Model & Throughput Control and Custom Thread Groups & Load Shaping.

Some things are not shared between workers, because each worker is a separate JVM:

  • a Critical Section Controller locks only inside one JVM;

  • a Synchronizing Timer releases the threads of one worker, not of all workers;

  • counters, __counter, __threadNum and CSV Data Set Config cursors start from the beginning on every worker, so two workers read the same lines of the same CSV file unless the files differ.

What travels to the workers and what does not

Item Sent by the controller? Notes

The test plan tree

Yes

No need to copy the .jmx file to the workers. Relative paths in the plan are resolved on the worker, relative to the plan’s base directory as the controller sends it.

-G properties (or a -G properties file)

Yes

Applied on each worker before the test starts. Read in the plan with ${__P(threads,1)}.

-J properties

No

Local to the controller JVM only. A -Jthreads=25 on the controller does not change what the workers run.

CSV files, payload files, certificates, protos, JDBC drivers

No

The manual is explicit: data files are not sent. They must exist at the expected path on every worker.

Plugins and extra jars (lib/, lib/ext/)

No

Every worker needs the same plugins at the same versions as the controller.

JMeter and Java versions

No

The manual asks for exactly the same JMeter version and the same Java version on all nodes.

user.properties and system.properties of each worker

No (local)

Read when the worker starts. Useful for per-worker values, such as a different data file per worker.

A remote start, step by step

The sequence below follows what 5.6.3 does when the controller is started with -R (from DistributedRunner and ClientJMeterEngine in the source code).

sequenceDiagram participant C as Controller (jmeter -n -R) participant R as Worker RMI registry (:1099) participant W as Worker engine (:server.rmi.localport) participant S as Bookshelf API C->>R: look up JMeterEngine (TLS, client certificate) R-->>C: remote engine stub, advertising java.rmi.server.hostname Note over C,R: repeated for every host in -R, retried client.tries times C->>C: export result listeners on client.rmi.localport C->>W: rconfigure (plan tree, host and port, base directory) C->>W: rsetProperties (the -G properties) C->>W: rrunTest loop every thread of every Thread Group W->>S: requests S-->>W: responses end W-->>C: sample batches (StrippedBatch: 100 samples or 60 s) W-->>C: test ended on host and port C->>C: write results.jtl and the dashboard (-e -o) C->>W: rexit (only with -X)

Two consequences follow from this sequence. First, the connection is opened in both directions: the controller calls the worker, and the worker calls back to listener objects exported by the controller. A firewall that only allows controller-to-worker traffic lets the test start but no results arrive. Second, the -G properties are set after the plan is configured and before it runs, which is why ${__P(...)} in Thread Group fields picks them up.

Setup step by step

The steps below set up two workers, gen1.internal and gen2.internal, and a controller, all on Linux or macOS, to run bookshelf-load.jmx with 25 threads per worker. Replace the names with your own hosts; on Windows the scripts end in .bat.

Step 0: prepare every node

On the controller and on every worker:

  1. Install the same JMeter version (5.6.3) and the same Java version (for example Temurin 21) as described in Installation & Configuration.

  2. Install the same plugins at the same versions, for example with the Plugins Manager command line and install-for-jmx (see Plugins & Plugins Manager).

  3. Copy the data files to the same path on every worker, for example /opt/bookshelf-perf/data/users.csv and /opt/bookshelf-perf/data/search-terms.csv. Pass that directory as a -G property so the plan does not depend on the working directory of each worker.

  4. Make sure the controller can reach every worker on the RMI ports, and every worker can reach the controller back (see the section on firewalls below).

  5. Synchronise the clocks (NTP). Timestamps in the results file come from the workers, and the dashboard’s time-based graphs assume they agree.

If every worker needs different data (unique user accounts, for example), split the file and give each worker its own part. The manual suggests exactly that: different property values in each worker’s user.properties, or different content in the data files.

# Split 2,000 placeholder accounts into two files, one per worker (header kept in each)
head -1 users.csv > users-gen1.csv; head -1 users.csv > users-gen2.csv
tail -n +2 users.csv | awk 'NR % 2 == 1' >> users-gen1.csv
tail -n +2 users.csv | awk 'NR % 2 == 0' >> users-gen2.csv
scp users-gen1.csv gen1.internal:/opt/bookshelf-perf/data/users.csv
scp users-gen2.csv gen2.internal:/opt/bookshelf-perf/data/users.csv

Step 1: create the RMI keystore and copy it to every node

Since 4.0, RMI between controller and workers uses TLS by default, and JMeter does not ship keys. The simplest setup is one key pair shared by all nodes. bin/create-rmi-keystore.sh (or .bat) is a thin wrapper around keytool: it generates a 2048-bit RSA key with the alias rmi, valid for 7 days, in rmi_keystore.jks with the password changeit, and passes any extra arguments to keytool. Run it interactively as in the manual, answering rmi as the first and last name, or non-interactively by passing -dname:

cd apache-jmeter-5.6.3/bin
./create-rmi-keystore.sh -dname "CN=rmi, OU=Performance, O=Bookshelf, L=Example City, ST=Example State, C=XY"

Output on 5.6.3 with Java 21:

Generating 2,048 bit RSA key pair and self-signed certificate (SHA384withRSA) with a validity of 7 days
	for: CN=rmi, OU=Performance, O=Bookshelf, L=Example City, ST=Example State, C=XY
Copy the generated rmi_keystore.jks to jmeter/bin folder or reference it in property 'server.rmi.ssl.keystore.file'

Then copy the same rmi_keystore.jks to every worker and to the controller, either into bin/ or anywhere else with server.rmi.ssl.keystore.file pointing at it. Three details matter in practice:

  • The default path is relative to the working directory of the JMeter process, not to bin/. A controller started from another directory with the default server.rmi.ssl.keystore.file=rmi_keystore.jks failed in the test run with java.io.FileNotFoundException: rmi_keystore.jks (No such file or directory). Set an absolute path in user.properties on every node.

  • Seven days is short. A keystore created on Monday breaks the following Monday’s test with a TLS handshake error. For a longer-lived environment, run the same keytool command as the script directly, with a longer -validity: keytool -genkey -keyalg RSA -alias rmi -keystore rmi_keystore.jks -storepass changeit -validity 90 -keysize 2048 (and a real password, set in server.rmi.ssl.keystore.password on every node).

  • The file is PKCS12 despite its name. Since Java 9, keytool creates PKCS12 keystores by default; keytool -list on the generated file reports Keystore type: PKCS12. JMeter’s default server.rmi.ssl.keystore.type=JKS still reads it on Java 21, because the JDK’s JKS keystore type also loads PKCS12 files. Setting server.rmi.ssl.keystore.type=PKCS12 makes the intent explicit.

Treat the keystore as a credential. Anyone holding it can connect to a worker and make it run an arbitrary plan, which includes OS Process Samplers and Groovy scripts. Never commit it to version control; create it per environment and distribute it like any other secret (for example as a Kubernetes Secret, see Secrets).

Step 2: start the workers

On each worker:

cd /opt/bookshelf-perf
/opt/apache-jmeter-5.6.3/bin/jmeter-server \
  -Djava.rmi.server.hostname=10.0.1.11 \
  -Jserver.rmi.localport=4000 \
  -Jserver.rmi.ssl.keystore.file=/opt/bookshelf-perf/rmi_keystore.jks

What the options do:

  • jmeter-server runs jmeter -Dserver_port=${SERVER_PORT:-1099} -s -j jmeter-server.log plus your arguments, so the worker starts its own RMI registry on port 1099 (or on SERVER_PORT if set), and logs to jmeter-server.log in the current directory.

  • -Djava.rmi.server.hostname is the address the worker advertises to the controller, which is explained under NAT below. Without it, JMeter uses the address the host name resolves to, and refuses to start if that is a loopback address.

  • -Jserver.rmi.localport fixes the port of the remote engine object; without it the port is random.

A worker that started correctly prints a "Created remote object" line. From the test run (loopback addresses, a non-default registry port and the keystore path shortened):

Using local port: 24000
Created remote object: UnicastServerRef2 [liveRef: [endpoint:[127.0.0.1:24000,SSLRMIServerSocketFactory(host=/127.0.0.1, keyStoreLocation=.../rmi_keystore.jks, type=JKS, ...

and its jmeter-server.log ends with:

INFO o.a.j.r.SSLRMIServerSocketFactory: Created SSLSocket: [SSL: ServerSocket[addr=/127.0.0.1,localport=24000]]
INFO o.a.j.e.RemoteJMeterEngineImpl: Starting backing engine on 21099
INFO o.a.j.e.RemoteJMeterEngineImpl: Creating RMI registry (server.rmi.create=true)
INFO o.a.j.e.RemoteJMeterEngineImpl: Bound to RMI registry on port 21099

Only one JMeter server can run per host unless each uses a different registry port (SERVER_PORT=1664 jmeter-server, then host:1664 in the controller’s host list). A worker keeps running after a test and accepts the next one, unless the controller passes -X or the worker sets server.exitaftertest=true. Its console prints one line when a test starts and one when it ends:

Starting the test on host 127.0.0.1:21105 @ 2026 Oct 14 13:31:47 CEST (1791977507439)
Finished the test on host 127.0.0.1:21105 @ 2026 Oct 14 13:31:59 CEST (1791977519425)

Step 3: tell the controller where the workers are

Either list the workers in the controller’s user.properties, which the GUI’s Remote Start menu and -r use, or pass them on the command line with -R, which has the same effect as -r -Jremote_hosts=.... The default value of remote_hosts is 127.0.0.1, and a host without :port uses server.rmi.port (1099).

# bin/user.properties on the controller (JMeter properties)
remote_hosts=gen1.internal,gen2.internal
# with a non-default registry port: remote_hosts=gen1.internal:1664,gen2.internal:1664
server.rmi.ssl.keystore.file=/opt/bookshelf-perf/rmi_keystore.jks

java.rmi.server.hostname is a Java system property, not a JMeter property: in user.properties it would have no effect. Put it in bin/system.properties, whose entries JMeter sets as system properties at start-up, or pass -Djava.rmi.server.hostname=10.0.1.10 on the command line:

# bin/system.properties on the controller
java.rmi.server.hostname=10.0.1.10

Step 4: run the test from the CLI

The manual is explicit that GUI mode is for debugging; real distributed tests start from a CLI controller:

jmeter -n -t bookshelf-load.jmx \
  -R gen1.internal,gen2.internal \
  -Gthreads=25 -Grampup=60 -Gduration=660 \
  -Ghost=bookshelf.internal -Gport=8080 \
  -Gdata_dir=/opt/bookshelf-perf/data \
  -l results/bookshelf-dist.jtl -j results/controller.log \
  -f -e -o results/report \
  -X
  • -R lists the workers and overrides remote_hosts; -r uses remote_hosts as configured.

  • -G sends each property to every worker. -Gfile.properties (a value without =) sends every property in that file; a file that cannot be read is silently ignored, so check controller.log for "Setting Global properties from the file".

  • -l, -e and -o work on the controller, which is the only node that writes results.

  • -X asks every worker to exit at the end of the test. Use it when workers are started per test (CI, containers); leave it out when the same workers serve several runs.

A real run of the same command, scaled down to 5 threads per worker for 20 s against a local stub of the Bookshelf API (workers on ports 21099 and 21100 of the same machine):

Creating summariser <summary>
Created the tree successfully using bookshelf-load.jmx
Configuring remote engine: 127.0.0.1:21099
Configuring remote engine: 127.0.0.1:21100
Starting distributed test with remote engines: [127.0.0.1:21100, 127.0.0.1:21099] @ 2026 Oct 14 13:29:39 CEST (1791977379870)
summary +      1 in 00:00:01 =    1.1/s Avg:    61 Min:    61 Max:    61 Err:     0 (0.00%) Active: 1 Started: 1 Finished: 0
Remote engines have been started:[127.0.0.1:21100, 127.0.0.1:21099]
Waiting for possible Shutdown/StopTestNow/HeapDump/ThreadDump message on port 4447
summary +      2 in 00:00:12 =    0.2/s Avg:    22 Min:     4 Max:    41 Err:     0 (0.00%) Active: 10 Started: 10 Finished: 0
summary =      3 in 00:00:13 =    0.2/s Avg:    35 Min:     4 Max:    61 Err:     0 (0.00%)
summary +    200 in 00:00:07 =   29.7/s Avg:    10 Min:     2 Max:   223 Err:     2 (1.00%) Active: 5 Started: 10 Finished: 5
summary =    203 in 00:00:20 =   10.3/s Avg:    10 Min:     2 Max:   223 Err:     2 (0.99%)
summary +    140 in 00:00:01 =  100.1/s Avg:     4 Min:     2 Max:    29 Err:     0 (0.00%) Active: 0 Started: 10 Finished: 10
summary =    343 in 00:00:21 =   16.2/s Avg:     8 Min:     2 Max:   223 Err:     2 (0.58%)
Tidying up remote @ 2026 Oct 14 13:30:02 CEST (1791977402222)
... end of run

Three things in this output are specific to distributed runs:

  • Active: 10 Started: 10 with -Gthreads=5: the summariser counts the threads of all workers, which confirms that the load is threads × workers.

  • The bursts (2 samples in 12 s, then 200 in 7 s and 140 in 1 s): with the default StrippedBatch sender, workers send results in batches of 100 samples or every 60 s, so the controller’s live view lags and arrives in lumps. The load on the target was steady; the section on sample senders shows the same test with a smoother mode.

  • Thread names carry the worker: since 5.0 every thread name is prefixed with the engine’s host and port, as in this line of bookshelf-dist.jtl:

timeStamp,elapsed,label,responseCode,responseMessage,threadName,dataType,success,failureMessage,bytes,sentBytes,grpThreads,allThreads,URL,Latency,IdleTime,Connect
1791977382856,41,GET /api/books,200,OK,127.0.0.1:21099-Readers 1-1,text,true,,225,126,1,1,http://127.0.0.1:28080/api/books,53,0,33

The prefix makes the dashboard’s Active Threads Over Time correct for distributed runs. The grpThreads and allThreads columns, however, are counted per worker (they were 5 in this run, never 10), so a step analysis that groups samples by allThreads must group by worker too, or count the workers' threads together. The dashboard’s Time Vs Threads graph has the same per-server limitation, which the manual documents.

Step 5 (optional): start from the GUI to check the setup

For a first check of the network and the keystore, the GUI is convenient. With remote_hosts set, the Run menu gains these entries:

Menu item Effect

Run → Remote Start → host

Sends the plan to one worker and starts it.

Run → Remote Start All (Ctrl+Shift+R)

Starts the plan on every host in remote_hosts.

Run → Remote Stop → host / Remote Stop All

Stops the test now on one or all workers (like Stop).

Run → Remote Shutdown → host / Remote Shutdown All

Lets current samples finish, then stops (like Shutdown).

Run → Remote Exit → host / Remote Exit All

Makes the worker process exit.

Do not use the GUI controller for the measured run: GUI listeners on the controller then process every sample from every worker.

Command-line options for distributed runs

Option Long form Meaning

-s

--server

Run JMeter in server (worker) mode; jmeter-server uses it.

-r

--runremote

Start the test on every host in remote_hosts.

-R host1,host2

--remotestart

Start the test on these hosts; overrides remote_hosts.

-Gname=value

--globalproperty

Define a JMeter property on every worker (not on the controller). May be repeated.

-Gfile.properties

--globalproperty

Send every property in a local file to every worker.

-X

--remoteexit

Make the workers exit at the end of a CLI test.

-Jname=value

--jmeterproperty

Define a property on the local JVM only: on the controller for sender settings such as mode, or on a worker when starting jmeter-server.

-Dname=value

--systemproperty

Define a Java system property locally, for example java.rmi.server.hostname.

-r, -R and -X are accepted only together with -n; without it JMeter prints "-r and -R and -X are only valid in non-GUI mode" and stops. The full list of options is on Running Tests in CLI Mode.

RMI properties

All of these are JMeter properties: put them in user.properties on the node they apply to, or pass them with -J when starting that node. Defaults are those of 5.6.3.

Property Node Default Meaning

remote_hosts

Controller

127.0.0.1

Comma-separated workers, each host or host:port, used by -r and the GUI’s remote menus.

server_port

Worker

1099 (set by jmeter-server from SERVER_PORT)

Port of the RMI registry the worker creates. When it is not set, server.rmi.port is used.

server.rmi.port

Both

1099

Default registry port: used by the worker when server_port is not set, and by the controller for hosts listed without a port.

server.rmi.localport

Worker

0 (random)

Fixed port for the worker’s remote engine object, so a firewall can allow it. The properties reference shows 4000, which is the commented example in jmeter.properties; the 5.6.3 source uses 0 when the property is unset.

server.rmi.create

Worker

true

The worker creates the RMI registry itself (since 2.3.1). Set to false only to use an external rmiregistry, as in the manual’s "Doing it manually" section.

server.exitaftertest

Worker

not set

Set to true to make the worker exit after one test, like -X but decided on the worker side.

client.rmi.localport

Controller

0 (random)

Base for the ports of the listener objects the controller exports for the workers' callbacks. The manual says JMeter opens up to three ports beginning with this one; in the 5.6.3 test run the controller used base + 1 and base + 2. See the firewall section for a limitation with SSL.

client.tries

Controller

1

Total number of attempts to configure each worker.

client.retries_delay

Controller

5000

Milliseconds between attempts.

client.continue_on_fail

Controller

false

Start the test with the workers that could be configured instead of failing.

server.rmi.ssl.disable

Both

false

Disable TLS for RMI. Must be the same on the controller and every worker.

server.rmi.ssl.keystore.type

Both

JKS

Keystore type (commonly JKS or PKCS12).

server.rmi.ssl.keystore.file

Both

rmi_keystore.jks

Keystore with the private key. Relative to the process working directory.

server.rmi.ssl.keystore.password

Both

changeit

Keystore password.

server.rmi.ssl.keystore.alias

Both

rmi

Alias of the key to use; must match the alias created by the script.

server.rmi.ssl.truststore.type

Both

the keystore type

Truststore type.

server.rmi.ssl.truststore.file

Both

the keystore file

Truststore with the trusted certificate. With one shared key pair, keystore and truststore are the same file.

server.rmi.ssl.truststore.password

Both

the keystore password

Truststore password.

jmeterengine.remote.system.exit

Worker

false

Call System.exit(0) in the worker after stopping RMI on exit; useful if a plugin leaves non-daemon threads behind.

And one Java system property, set with -D or in system.properties:

System property Node Meaning

java.rmi.server.hostname

Both

The address a node advertises in the RMI stubs it exports, and (with SSL) the address JMeter binds its RMI server sockets to. When it is unset, JMeter uses the address of the local host name and refuses to start if that is a loopback address; when it is set, a loopback address is accepted (which is how SSH tunnelling works).

RMI over SSL

How it works

Every RMI connection between controller and workers, the registry included, is a TLS connection. The server side requires a client certificate (mutual TLS has been enforced since 5.1), and both sides validate the peer against the truststore. With the shared keystore from create-rmi-keystore.sh, each node presents the same self-signed certificate and trusts it because it is in its own truststore. A node with another certificate is rejected.

The test run produced each failure mode in turn:

Situation Controller output (5.6.3) Fix

Keystore file not found on the controller

Exception creating connection to: 127.0.0.1; nested exception is: java.io.FileNotFoundException: rmi_keystore.jks (No such file or directory)

Absolute path in server.rmi.ssl.keystore.file.

Controller and worker have different keystores

error during JRMP connection establishment; nested exception is: javax.net.ssl.SSLHandshakeException: PKIX path building failed: ... unable to find valid certification path to requested target

Copy the same rmi_keystore.jks to every node (or put both certificates in the truststores).

Worker not running or wrong port

Connection refused to host: 127.0.0.1; nested exception is: java.net.ConnectException: Connection refused

Start jmeter-server; check remote_hosts and server_port.

In each case the controller then prints Failed to configure host:port and stops with Following remote engines could not be configured, unless client.continue_on_fail is set. An expired certificate (after the seven days) shows up as another TLS handshake failure; create a new keystore and copy it again.

Source: Remote Testing: Setting up SSL (User Manual) and Changes History (4.0: SSL for RMI; 5.1: client authentication enabled)

Disabling SSL, and what it costs

Setting server.rmi.ssl.disable=true on every node (controller and workers) turns RMI back into plain TCP, which is how distributed testing worked before 4.0 and how the books describe it:

# user.properties on the controller and on every worker
server.rmi.ssl.disable=true

The log then shows Disabling SSL for RMI as server.rmi.ssl.disable is set to 'true'. The cost is security, not performance. A JMeter server is, in the manual’s words, basically a remote execution agent: anyone who can reach port 1099 and the engine port of an unprotected worker can send it a plan with an OS Process Sampler or a Groovy script and run commands on that machine, with the worker’s user rights. Disable SSL only on isolated, disposable networks (a private lab subnet, containers on a private network that is torn down after the test), never on a shared or reachable network. Mixing settings (SSL on one side, off on the other) fails at connection time.

The manual’s section 13.7 additionally describes running JMeter under a Java security manager with a policy file. The Java Security Manager is deprecated for removal since Java 17 (JEP 411) and permanently disabled since Java 24 (JEP 486), so on current Java versions network isolation and TLS are the controls that remain.

Firewalls and ports

RMI needs connections in both directions. With fixed ports, the rules are:

Direction Destination port Property Purpose

Controller → worker

1099

server_port / SERVER_PORT (worker), remote_hosts (controller)

Registry lookup of the JMeterEngine object.

Controller → worker

for example 4000

server.rmi.localport (worker)

Calls on the remote engine: configure, set properties, run, stop, exit. Random if unset.

Worker → controller

client.rmi.localport + 1 and + 2 (open the range base to base + 2)

client.rmi.localport (controller)

Callbacks that carry sample results and thread start/stop notifications. Random if unset.

Worker → target

the application’s ports

plan

The load itself.

A measured limitation with SSL. In the 5.6.3 test run, setting client.rmi.localport on the controller while SSL was enabled made every remote start fail, even with a single worker. The controller exports more than one listener object on port base + 2 (the summariser’s result collector and the end-of-test listener), and with TLS the second export fails:

ERROR o.a.j.e.ConvertListeners: RemoteException occurred while replacing Remotable item.
java.rmi.server.ExportException: Listen failed on port: 26002; nested exception is:
	java.io.IOException: Could not bind to /127.0.0.1 using port 26002
...
Error in rconfigure() method java.rmi.MarshalException: error marshalling arguments; nested exception is:
	java.io.NotSerializableException: org.apache.jmeter.JMeter$ListenToTest
Remote engines have been started:[]
The following remote engines have not started:[127.0.0.1:21099]

The NotSerializableException is a consequence of the failed export (the listener could not be replaced by its remote stub), not a separate problem. The same run with server.rmi.ssl.disable=true and the same fixed port worked with two workers, and with SSL and the default client.rmi.localport=0 it also worked, on Java 21 and on Java 25. If this error appears, leave client.rmi.localport at 0 and allow the workers to reach the controller’s ephemeral ports (put the controller in the same subnet or security group as the workers), or disable SSL on an isolated network. Note also that in this failure the controller’s exit code was 0 even though no worker started, so a CI job must check that the results file is not empty.

The step-by-step tutorial adds two network constraints of RMI itself: RMI does not cross subnets without a proxy, and the workers should be on the same network segment as the system under test, so that the network between them does not dominate the measurements. Never run a worker on the server under test: it competes with the application for CPU.

NAT, several interfaces and java.rmi.server.hostname

An RMI stub contains the host and port at which the exporting JVM can be called. A worker puts its own address in the engine stub; the controller puts its own address in the listener stubs it sends to the workers. If that address is wrong from the other side’s point of view, the first connection works (the controller reaches the registry) and the second fails with Connection refused to host: <some address>, often a private or loopback address.

Typical cases and the setting that fixes them:

Situation Setting

Worker host name resolves to 127.0.0.1 or 127.0.1.1 (common on Linux /etc/hosts)

On the worker: -Djava.rmi.server.hostname=<its LAN address>. jmeter-server has a commented RMI_HOST_DEF variable for exactly this. Without it, 5.6.3 refuses to start with "is a loopback address".

Machine with several interfaces (VPN, Docker bridge)

Set java.rmi.server.hostname to the address on the network the other side uses. With SSL enabled, JMeter also binds its RMI server sockets to that address (the log shows ServerSocket[addr=/...]).

Worker behind NAT (cloud VM with a public IP, container with port mapping)

On the worker: the address the controller connects to (the public or mapped address), and the same port numbers on both sides of the NAT for server_port and server.rmi.localport, because the port in the stub is not translated.

Controller behind NAT or on a laptop on VPN

On the controller: an address the workers can reach, plus a fixed client.rmi.localport range forwarded to it (with the SSL limitation described above). In practice, run the controller next to the workers instead.

SSH tunnel

Forward the ports and set java.rmi.server.hostname=127.0.0.1 on both ends; loopback is allowed when the property is set explicitly (since 2.6).

The startup log shows which address a node chose:

INFO o.a.j.r.RmiUtils: Resolving by name the value of System property 'java.rmi.server.hostname': 127.0.0.1
INFO o.a.j.r.RmiUtils: Local IP address=127.0.0.1

When the property is unset and the address is a site-local one (10.x, 172.16-31.x, 192.168.x), the worker logs "IP address is a site-local address; this may cause problems with remote access", which is a hint, not an error.

Results: sample sender modes

Listeners in the plan run on the controller, so every sample a worker produces has to be sent back over RMI. How it is sent is decided by the mode property, the sample sender. The choice trades controller load and network traffic against how much detail and how much immediacy the controller gets.

The modes

mode Behaviour Trade-off

StrippedBatch (default since 2.9)

Removes response data from samples (also from failed ones since 3.1), then sends them in batches when num_sample_threshold samples (100) have accumulated or time_threshold (60 s) has passed.

Low network and controller load. Live summary arrives in bursts. No response bodies on the controller.

Standard

Sends every sample synchronously as soon as it is produced.

Smooth live view and full samples, but the sampler thread waits for the RMI call, which lowers worker throughput and loads the controller with one call per sample.

Stripped

Like Standard, with response data removed.

Smooth live view, less traffic; still one call per sample.

Batch

Like StrippedBatch, but keeps response data.

Full samples; large batches if responses are big.

Statistical

Sends a summary per Thread Group and label (or per thread name with key_on_threadname=true) when the thresholds are reached. Only elapsed time, latency, bytes, sample count and error count are accumulated.

Lowest traffic. Individual samples, percentiles and per-request details are lost; the results file holds aggregated rows (SampleCount above 1), so the dashboard’s percentiles are not real percentiles.

Asynch

Puts samples in a queue on the worker; a background thread sends them, so the sampler thread does not wait. When the queue (asynch.batch.queue.size, 100) is full, sampler threads block.

Smooths peaks; samples are sent one by one, so traffic is like Standard.

StrippedAsynch

Asynch with response data removed.

As above, with less traffic.

DiskStore

Serialises samples to a temporary file on the worker and sends them at the end of the test.

No result traffic during the test, but no live view, and results are lost if a worker dies.

StrippedDiskStore

DiskStore with response data removed.

As above, with smaller files.

class name

A custom class implementing SampleSender with a constructor taking a RemoteSampleListener.

For special needs only.

Hold (removed)

Held samples in memory until the end. Removed in 4.0, although the remote testing page still lists it.

In 5.6.3 -Jmode=Hold fails: Unable to create a sample sender from mode or class:'Hold' (exit code 1).

"Stripped" means that elements on the controller that need response bodies, such as a View Results Tree used for debugging, show empty responses. Assertions and extractors are not affected, because they run on the workers where the full response is still available.

Where the settings are read

The manual says that mode is resolved on the controller and the other properties on the server. In 5.6.3 this is governed by sample_sender_client_configured, which defaults to true, and then the thresholds too are taken from the controller. The test run confirmed it: with -Jnum_sample_threshold=50 -Jtime_threshold=5000 on the controller only, each worker logged:

INFO o.a.j.s.BatchSampleSender: Using batching for this run. Thresholds: num=50, time=5000

So in practice: set every sender property on the controller with -J (or in its user.properties). Set sample_sender_client_configured=false on the controller only if each worker should use its own num_sample_threshold, time_threshold and asynch.batch.queue.size.

Property Default Meaning

mode

StrippedBatch

Sample sender, as in the table above.

num_sample_threshold

100

Batch and Statistical: number of samples before sending.

time_threshold

60000

Batch and Statistical: milliseconds before sending.

key_on_threadname

false

Statistical: key the summaries on thread name instead of Thread Group.

asynch.batch.queue.size

100

Asynch modes: queue size on the worker.

sample_sender_client_configured

true

Use the controller’s values for the properties above.

sample_sender_strip_also_on_error

true

Stripped modes: strip failed samples too (set false to keep error bodies, as before 3.1).

The same test with two modes

Both runs below used two workers, 5 threads each, a 500 ms think time and a 5 s summariser interval against the same local stub. Only the sender changed.

-Jmode=Standard:

summary +      1 in 00:00:01 =    1.4/s Avg:    29 Min:    29 Max:    29 Err:     0 (0.00%) Active: 2 Started: 2 Finished: 0
summary +     76 in 00:00:06 =   13.0/s Avg:     3 Min:     1 Max:    38 Err:     0 (0.00%) Active: 10 Started: 10 Finished: 0
summary =     77 in 00:00:07 =   11.7/s Avg:     3 Min:     1 Max:    38 Err:     0 (0.00%)
summary +    113 in 00:00:06 =   19.3/s Avg:     2 Min:     1 Max:     6 Err:     0 (0.00%) Active: 0 Started: 10 Finished: 10
summary =    190 in 00:00:12 =   15.3/s Avg:     2 Min:     1 Max:    38 Err:     0 (0.00%)

Default (StrippedBatch), same plan and duration:

summary +      1 in 00:00:01 =    1.4/s Avg:    32 Min:    32 Max:    32 Err:     0 (0.00%) Active: 2 Started: 2 Finished: 0
summary +      2 in 00:00:11 =    0.2/s Avg:    20 Min:     3 Max:    38 Err:     0 (0.00%) Active: 1 Started: 10 Finished: 9
summary =      3 in 00:00:12 =    0.3/s Avg:    24 Min:     3 Max:    38 Err:     0 (0.00%)
summary +    187 in 00:00:00 = 1106.5/s Avg:     2 Min:     1 Max:    18 Err:     0 (0.00%) Active: 0 Started: 10 Finished: 10
summary =    190 in 00:00:12 =   15.8/s Avg:     3 Min:     1 Max:    38 Err:     0 (0.00%)

Both runs made 190 requests at about 15 to 16 req/s, and the final summary = lines agree. The difference is only in the controller’s live view: with batching, almost every sample arrived in the last second, so the interval line shows a meaningless 1,106.5/s. For long tests that is harmless (batches of 100 arrive continuously), but in short tests, or when watching the run, lower time_threshold (for example 5000) or use the Backend Listener for live monitoring, as described in Real-time Monitoring with Backend Listener.

Worked example: result traffic. At 625 req/s in total across four workers, Standard means 625 RMI calls per second into the controller, each carrying one sample, and every sampler thread waits for its call. StrippedBatch with the defaults means about 625 / 100 ≈ 6 calls per second, each with 100 stripped samples. That is why the default is batched, and why the manual warns that a controller with many workers can still become overloaded: check its CPU and network like a generator’s.

Handling failed workers

On large tests, especially with machines created on demand, some workers may boot late or not at all. By default the controller makes one attempt per worker and aborts the whole test if any worker cannot be configured. A real run with one worker missing:

Configuring remote engine: 127.0.0.1:21107
Configuring remote engine: 127.0.0.1:21199
Connection refused to host: 127.0.0.1; nested exception is:
	java.net.ConnectException: Connection refused
Failed to configure 127.0.0.1:21199
Stopping remote engines
Remote engines have been stopped
Error in NonGUIDriver java.lang.RuntimeException: Following remote engines could not be configured:[127.0.0.1:21199]
An error occurred: Error in NonGUIDriver Following remote engines could not be configured:[127.0.0.1:21199]

The exit code was 1. With retries and continue_on_fail (property values in user.properties, or on the command line):

jmeter -n -t bookshelf-load.jmx -R gen1.internal,gen2.internal \
  -Jclient.tries=3 -Jclient.retries_delay=2000 -Jclient.continue_on_fail=true \
  -Gthreads=25 -Grampup=60 -Gduration=660 -l results/bookshelf-dist.jtl -X

the same situation produced:

Failed to configure 127.0.0.1:21199
Following remote engines will retry configuring: [127.0.0.1:21199], pausing before retry for 2000ms
Configuring remote engine: 127.0.0.1:21199
...
Failed to configure 127.0.0.1:21199
Following remote engines could not be configured:[127.0.0.1:21199]
Continuing without failed engines...
Starting distributed test with remote engines: [127.0.0.1:21108] @ 2026 Oct 14 13:32:40 CEST (1791977560561)
Remote engines have been started:[127.0.0.1:21108]
...
Tidying up remote @ 2026 Oct 14 13:32:46 CEST (1791977566068)
Exiting remote servers:[ClientJMeterEngine [hostAndPort=127.0.0.1:21108]]
Exiting remote server:ClientJMeterEngine [hostAndPort=127.0.0.1:21108]
... end of run

and exit code 0. client.tries is the total number of attempts (3 means two retries), and the delay applies between rounds, so the start was delayed by about 4 s. If no worker at all could be configured, the test fails even with continue_on_fail.

Be deliberate about continue_on_fail. A test that silently runs with three workers out of four delivers 75 % of the planned load, and the report looks valid. When using it, check the Remote engines have been started line, or the number of distinct worker prefixes in the thread names of the results file, before trusting the numbers:

awk -F, 'NR>1 {split($6, a, "-"); print a[1]}' results/bookshelf-dist.jtl | sort | uniq -c
    173 127.0.0.1:21099
    170 127.0.0.1:21100

These properties only cover start-up. A worker that dies during the test is not replaced; its samples simply stop. The controller waits for the remaining workers and then ends. Watch the per-worker sample counts over time (or a Backend Listener tagged by generator) to notice it.

Per-worker data and behaviour

A few patterns keep distributed runs correct:

  • Unique data per worker. Split CSV files as in step 0, or keep one file per worker with the same name. A property-driven alternative: set users_file differently in each worker’s user.properties and point the CSV Data Set Config at ${__P(users_file,users.csv)}.

  • Identify the generator in the data. ${__machineName()} and ${__machineIP()} return the worker’s host name and address, for example to tag a request header or a Backend Listener field (see Real-time Monitoring with Backend Listener for why the tag is needed there).

  • Absolute paths for data. Pass -Gdata_dir=/opt/bookshelf-perf/data and use ${__P(data_dir)}/users.csv in the CSV Data Set Config, so each worker finds the file regardless of the directory jmeter-server was started in.

  • Same JDBC driver, same plugins, same certificates. Anything in lib/, lib/ext/ or referenced by a keystore property must be present on every worker (Database Testing with JDBC, gRPC Load Testing).

  • Result file settings belong to the controller. jmeter.save.saveservice.* properties apply where the file is written; set them on the controller. Variables do not work in a listener’s file name in distributed mode, but functions do.

Load-generator cost

Node What it costs

Worker

The same as a standalone CLI run of the plan, plus serialising samples for RMI. With Standard or Asynch modes, sampler threads also wait for (or queue behind) result calls, which reduces the load a worker can produce.

Controller

No load generation, but it deserialises every sample from every worker, runs every listener in the plan (summariser, result file writer, Backend Listener if placed there) and generates the dashboard. Its network inbound traffic is the sum of all workers' result traffic.

Network

RMI result traffic between workers and controller shares links with the test traffic if they are on the same interface. The step-by-step tutorial asks to monitor it so it does not cause contention.

The manual also notes that remote mode uses more resources than running the same number of CLI tests independently, which is the main argument for the alternatives below.

Alternatives to RMI

Independent CLI instances with merged results

Instead of a controller, run the same plan in CLI mode on each generator at the same time, collect the results files and merge them. This avoids RMI, keystores, callback ports and the controller bottleneck completely, and it is the usual way to run JMeter in containers. What it gives up: a single live summary, a single start command, and coordinated stop (each instance stops on its own schedule, or with its own shutdown.sh).

The steps:

  1. Start every instance with the same plan and properties, and a per-instance results file. Use the scheduler duration (-Jduration) so all instances stop together if they start together.

  2. Give each instance a distinct thread-name prefix, otherwise the dashboard’s Active Threads Over Time counts thread "Readers 1-1" of every instance as one thread. 5.6.3 builds remote thread names from an internal property, __jm.D_TG, that the remote engine sets to its host:port. Setting it with -J on a standalone instance gives the same prefix. It is not in the properties reference, so treat it as an implementation detail that may change; the alternative is a different Thread Group name per instance.

  3. Merge the CSV files: keep one header line, append the data lines of all files and sort them by timestamp.

  4. Generate the dashboard from the merged file with -g.

A real run of two instances on one machine (15 s, 5 threads each):

jmeter -n -t bookshelf-load.jmx -Jthreads=5 -Jrampup=5 -Jduration=15 -J__jm.D_TG=gen-a -l gen-a.jtl -j gen-a.log &
jmeter -n -t bookshelf-load.jmx -Jthreads=5 -Jrampup=5 -Jduration=15 -J__jm.D_TG=gen-b -l gen-b.jtl -j gen-b.log &
wait

head -1 gen-a.jtl > merged.jtl
tail -q -n +2 gen-a.jtl gen-b.jtl | sort -t, -k1,1n >> merged.jtl
awk -F, 'NR>1 {split($6, a, " "); print a[1]}' merged.jtl | sort | uniq -c
jmeter -g merged.jtl -o report
    125 gen-a-Readers
    125 gen-b-Readers

and jmeter -g produced report/index.html and report/statistics.json with exit code 0. The merge only works when every instance writes the same columns, so use the same jmeter.save.saveservice.* settings everywhere. Percentiles must be computed from the merged raw file, never averaged from per-instance reports.

Workers in containers and Kubernetes

There is no official Apache JMeter Docker image and the JMeter manual says nothing about containers or Kubernetes; the patterns below are general container practice applied to JMeter, not an official recipe. Build your own image from the official binary, verified against its published SHA-512:

FROM eclipse-temurin:21-jre
ARG JMETER_VERSION=5.6.3
ARG JMETER_SHA512=5978a1a35edb5a7d428e270564ff49d2b1b257a65e17a759d259a9283fc17093e522fe46f474a043864aea6910683486340706d745fcdf3db1505fd71e689083
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/* \
 && curl -fsSLo /tmp/jmeter.tgz https://archive.apache.org/dist/jmeter/binaries/apache-jmeter-${JMETER_VERSION}.tgz \
 && echo "${JMETER_SHA512}  /tmp/jmeter.tgz" | sha512sum -c - \
 && tar -xzf /tmp/jmeter.tgz -C /opt && rm /tmp/jmeter.tgz \
 && ln -s /opt/apache-jmeter-${JMETER_VERSION}/bin/jmeter /usr/local/bin/jmeter
WORKDIR /tests
ENTRYPOINT ["jmeter"]

The checksum is the one published for 5.6.3 on the download page; plugins can be added in further RUN steps with the Plugins Manager command line, which keeps every generator identical by construction.

Independent instances as an Indexed Job. The simplest Kubernetes form of the previous section: one Job, N pods, each running the plan in CLI mode with its index, writing to a shared volume. Indexed Jobs set JOB_COMPLETION_INDEX in every pod.

apiVersion: batch/v1
kind: Job
metadata:
  name: bookshelf-load
  namespace: perf
spec:
  completions: 4
  parallelism: 4
  completionMode: Indexed
  backoffLimit: 0
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: jmeter
          image: registry.example.internal/perf/jmeter:5.6.3
          args:
            - "-n"
            - "-t"
            - "/tests/bookshelf-load.jmx"
            - "-Jthreads=500"
            - "-Jrampup=60"
            - "-Jduration=660"
            - "-Jhost=bookshelf-api.bookshelf.svc.cluster.local"
            - "-Jport=8080"
            - "-Jusers_file=/data/users-$(JOB_COMPLETION_INDEX).csv"
            - "-J__jm.D_TG=gen-$(JOB_COMPLETION_INDEX)"
            - "-l"
            - "/results/gen-$(JOB_COMPLETION_INDEX).jtl"
          resources:
            requests: { cpu: "2", memory: "2Gi" }
            limits: { cpu: "2", memory: "2Gi" }
          env:
            - name: HEAP
              value: "-Xms1g -Xmx1g -XX:MaxMetaspaceSize=256m"
          volumeMounts:
            - { name: plan, mountPath: /tests }
            - { name: data, mountPath: /data }
            - { name: results, mountPath: /results }
      volumes:
        - name: plan
          configMap: { name: bookshelf-plan }
        - name: data
          configMap: { name: bookshelf-users }
        - name: results
          persistentVolumeClaim: { claimName: jmeter-results }

Four pods with 500 threads each give 2,000 users, the worked example at the top of this page. Kubernetes expands $(JOB_COMPLETION_INDEX) in args because the Job controller adds that variable to each container’s environment. The results volume must support ReadWriteMany if the pods land on different nodes; after the Job completes, a short pod (or a step in CI) merges the files and runs jmeter -g as shown above. Equal CPU requests and limits keep generators comparable and avoid CPU throttling skewing timings; pod anti-affinity spreads them over nodes. The Kubernetes pages of this site cover the building blocks: Jobs and CronJobs (Indexed Jobs), ConfigMaps, Persistent Volumes and Storage Classes, Resource Management and Scheduling and Disruptions. To watch the application scale under the load, see Autoscaling.

RMI workers in Kubernetes. Classic controller and worker mode also works in a cluster if both ends live inside it, since pod IPs are directly routable between pods but not from a laptop outside the cluster. A StatefulSet with a headless Service gives workers stable DNS names (jmeter-worker-0.jmeter-worker.perf.svc.cluster.local and so on, see StatefulSets and Services), and the controller runs as a Job in the same namespace with -R listing those names. Each pod must advertise its own pod IP and use fixed ports:

# container fragment of the worker StatefulSet
args: ["-s", "-j", "/tmp/jmeter-server.log",
       "-Dserver_port=1099", "-Djava.rmi.server.hostname=$(POD_IP)",
       "-Jserver.rmi.localport=4000",
       "-Jserver.rmi.ssl.keystore.file=/rmi/rmi_keystore.jks"]
env:
  - name: POD_IP
    valueFrom:
      fieldRef: { fieldPath: status.podIP }
ports:
  - { name: registry, containerPort: 1099 }
  - { name: engine, containerPort: 4000 }
volumeMounts:
  - { name: rmi-keystore, mountPath: /rmi, readOnly: true }

The controller pod needs -Djava.rmi.server.hostname=$(POD_IP) too, and the keystore comes from a Secret mounted on every pod. If NetworkPolicies are in force, allow controller-to-worker traffic on 1099 and 4000 and worker-to-controller traffic on the controller’s ports (see Network Policies). For most container setups the independent-instance Job is simpler and scales better; choose RMI only when a single live summary on one controller is worth the extra moving parts.

For running the system under test and JMeter side by side on one machine, see Compose for Local Development and Docker Networking; remember that inside a container localhost is the container itself.

Cloud services

Several commercial and cloud services run JMeter plans on managed generators and merge the results, among them BlazeMeter, OctoPerf and Azure Load Testing. They take care of provisioning, distribution and reporting; check each service’s own documentation for the JMeter versions and plugins it supports and for how it splits threads and CSV files between engines. The JMeter Cookbook (ch. 5) shows recipes for Flood.io, Vagrant and Amazon EC2 from 2014; the services and tooling have changed since then, so use the recipes for the ideas only.

Pitfalls

  • Load multiplied by mistake. Thread counts, throughput timers and arrival rates are per worker. Divide the target by the number of workers.

  • -J instead of -G. A -Jthreads=25 on the controller is ignored by the workers, which then use the plan’s default from ${__P(threads,1)}.

  • Shell quoting. In zsh, an unquoted variable holding several options ($OPTS) is passed as one argument, so -Gthreads=5 -Grampup=5 becomes one property with a strange value and the workers run 0 threads. Use arrays or write the options out.

  • Missing files on workers. CSV files, payloads and plugins are not sent. A missing CSV file on one worker makes that worker’s requests run with wrong or empty values while the others look fine; check every worker’s jmeter-server.log for errors.

  • Keystore problems. Relative keystore path, keystore expired after seven days, different keystores per node, or SSL disabled on one side only.

  • One-way firewall. The test starts but no results arrive, because the callback connection from the workers to the controller is blocked.

  • Loopback or wrong address advertised. Set java.rmi.server.hostname (as a system property, not a JMeter property).

  • client.rmi.localport with SSL. In 5.6.3 it made the remote start fail; see the firewall section.

  • Statistical mode and percentiles. Aggregated rows make the dashboard’s percentiles meaningless.

  • Trusting the exit code. A run where no worker started can still end with exit code 0. Check the results file.

  • Silently reduced load. client.continue_on_fail=true can drop workers; count them.

  • Clock skew. Workers with unsynchronised clocks produce shifted timelines in the merged results.

From the books to 5.6 to 6.0

Topic Books (2.11 to 3.0) 5.6.3 and planned 6.0

Terminology

Master and slave

Controller and worker (client and server) in the manual

Transport security

Plain RMI; set up firewalls and go

RMI over SSL by default since 4.0, with a keystore from create-rmi-keystore.sh; client certificates required since 5.1; server.rmi.ssl.disable=true to opt out

Sample sender

StrippedBatch as default already (since 2.9); Hold available

StrippedBatch default; Hold removed in 4.0; stripping also applies to failed samples since 3.1

Thread names

Same names on every server; the reports needed extra configuration to count threads

Prefixed with the engine’s host:port since 5.0, so the dashboard counts threads correctly

Fixed callback ports

client.rmi.localport (since 2.6)

Same property; port allocation fixed in 5.0 and documented in 5.3 and 5.4.1; see the SSL limitation above

Failed nodes

client.tries, client.retries_delay, client.continue_on_fail (since 2.13)

Unchanged

Cloud

Flood.io, Vagrant and AWS recipes (Cookbook ch. 5)

Containers and Kubernetes with self-built images, or managed services

Java

Java 7 or 8

Java 8 or newer for 5.6.x (17 or newer recommended); 6.0 plans Java 17 as the minimum

The Spring Boot-specific wiring (Actuator metrics, the Maven plugin) is on Performance Testing with Apache JMeter. Running distributed or multi-instance tests from a pipeline, tuning each generator and securing load tests in general are covered on the later pages of this section about best practices, CI/CD and security.

References