Best Practices & Load Generator Tuning
|
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.
The JMeter user manual has a short chapter called Best Practices. Most of its advice fits in one sentence per item, and most of the reasons behind it are spread over the rest of the manual. This page walks through the chapter item by item, says why each item matters, measures the cost of ignoring some of them, and links to the page of this section that covers the details. It then adds what the chapter leaves out: JVM settings for a dedicated load generator, operating-system limits (open files, ephemeral ports, TIME_WAIT), a review checklist to run before a big test, and a table of the books' advice sorted into what still holds and what is outdated.
The page 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). JMeter 6.0 is not released; changes planned on the development
branch (Java 17 as the minimum, removal of the deprecated MongoDB elements, Groovy 5) are marked as planned. Plugins are
installed with the JMeter Plugins Manager 2.0 and CI runs use jmeter-maven-plugin 3.8.x, both covered on their own pages.
Operating-system values come from the Linux kernel documentation, the systemd manual, the Docker and Kubernetes
documentation and Microsoft Learn; JVM values from the Java 21 java command reference. The measurements on this page
were taken with the 5.6.3 binary distribution on Java 21. The books the section draws on (JMeter Cookbook, Performance
Testing with JMeter, Pro Apache JMeter) target JMeter 2.11 to 3.0; their advice is sorted at the end of the page.
The page assumes the material of Running Tests in CLI Mode, which already sizes heap, CPU, network, file descriptors and ephemeral ports with worked numbers, and of Installation & Configuration, which documents the launcher variables. It does not repeat them; it adds the persistent settings, the checks and the reasons.
Where tuning happens
A load test has four layers that can each become the bottleneck before the system under test does. Best practices address the first two; this page adds the last two.
Review them in that order: a plan that keeps every response in a View Results Tree will exhaust any heap, and no amount of kernel tuning compensates for a plan that writes XML results.
The official best practices, item by item
The chapter is section 16 of the user manual. The table maps each item to its essence and to the page that covers it in depth; the subsections below add what the chapter only implies.
| § | Item | What it says, in short | Covered in depth on |
|---|---|---|---|
16.1 |
Always use the latest version |
Performance improves with each release; read the changes list; avoid versions older than three versions before the latest. |
|
16.2 |
Use the correct number of threads |
Hardware, plan design and server speed limit the thread count; a wrongly sized generator causes coordinated omission; scale out with several CLI instances; measure a platform with the JavaTest sampler; delay thread creation. |
|
16.3, 16.4 |
Where to put the Cookie Manager and the Authorization Manager |
Pointers to the web test tutorials. |
|
16.5 |
Using the HTTP(S) Test Script Recorder |
Filter out everything you are not interested in; record into a Recording Controller; let user-defined variables
replace recorded values; the |
|
16.6 |
User variables |
Per-user logins from a CSV file read by a CSV Data Set Config, one line per thread. |
|
16.7 |
Reducing resource requirements |
CLI mode, few listeners, no functional mode, CSV output, save only what is needed, few assertions, one sampler in a loop fed by data, the fastest scripting language, test data prepared in files. |
This page, below |
16.8 |
BeanShell server |
It has no security: anyone who can reach the port can run any command. Do not enable it unless the ports are protected. |
This page, below |
16.9, 16.12 |
BeanShell scripting; JSR223 elements |
Since 3.1, use JSR223 elements and |
|
16.10 |
Developing script functions in Groovy or JEXL3 |
Develop the script in a JSR223 Sampler, then store it in a variable and call it with |
|
16.11 |
Parameterising tests |
Define Test Plan variables from properties with |
|
16.13 |
Sharing variables between threads and thread groups |
Variables are per thread by design; use properties, files, or your own classes. |
|
16.14 |
Managing properties |
Never edit |
|
16.15 |
Deprecated elements |
Do not use them; they are hidden from the menu in version N and removed in N+1. |
This page, below |
Source: Best Practices (User Manual)
16.1 Always use the latest version
The manual’s rule is short: use the most recent version and read the changes list, and avoid versions older than three versions before the latest one. It does not define "version", so treat it as a policy rather than a number: in practice, every generator, every developer machine and the CI image should run the same, current release (5.6.3 at the time of writing) with the same plugin versions, and an upgrade is a planned change with a re-baseline run, not something that happens silently between two test runs.
Check what a machine actually runs before trusting its numbers:
jmeter -v
_ ____ _ ____ _ _ _____ _ __ __ _____ _____ _____ ____
/ \ | _ \ / \ / ___| | | | ____| | | \/ | ____|_ _| ____| _ \
/ _ \ | |_) / _ \| | | |_| | _| _ | | |\/| | _| | | | _| | |_) |
/ ___ \| __/ ___ \ |___| _ | |___ | |_| | | | | |___ | | | |___| _ <
/_/ \_\_| /_/ \_\____|_| |_|_____| \___/|_| |_|_____| |_| |_____|_| \_\ 5.6.3
Copyright (c) 1999-2024 The Apache Software Foundation
Why it matters for results. Defaults change between releases: 5.3 moved the bundled Groovy to 3.0, 5.6.1 made UTF-8
the default HTTP encoding, and the planned 6.0 changes cookie, redirect and timer behaviour. Two runs on two versions
are two different experiments. Pin the version where it is installed (the CI image, the Maven <jmeterVersion>), and
record it with every report. The release history is in the
Changes History, and planned 6.0 changes in
changes.xml on master.
16.2 Use the correct number of threads
The manual names three factors: the generator’s hardware, the plan’s design, and the server’s speed, because a faster server returns responses sooner and therefore makes JMeter work harder. A generator that cannot keep up sends requests late without recording the delay, which is the coordinated omission problem explained on Performance Testing Fundamentals. When one machine is not enough, the manual recommends several CLI instances, either in distributed mode or as independent instances whose results files are merged afterwards; see Distributed Testing. The calibration loop that finds how many threads one machine holds is on Thread Groups: How many users one load generator can hold.
Measuring the ceiling of a platform with JavaTest. The manual suggests the JavaTest sampler for testing how JMeter
performs on a given platform: it needs no network access, so it shows the maximum sample rate the JVM, the plan
machinery and the results writer can sustain on that machine. Add → Sampler → Java Request and choose the class
org.apache.jmeter.protocol.java.test.JavaTest; set Sleep_Time and Sleep_Mask to 0 so each sample returns at once.
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Calibration" enabled="true">
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
<elementProp name="ThreadGroup.main_controller" elementType="LoopController"
guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
<boolProp name="LoopController.continue_forever">false</boolProp>
<intProp name="LoopController.loops">-1</intProp>
</elementProp>
<stringProp name="ThreadGroup.num_threads">${__P(threads,8)}</stringProp>
<stringProp name="ThreadGroup.ramp_time">${__P(rampup,1)}</stringProp>
<boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
<boolProp name="ThreadGroup.delayedStart">false</boolProp>
<boolProp name="ThreadGroup.scheduler">true</boolProp>
<stringProp name="ThreadGroup.duration">${__P(duration,30)}</stringProp>
<stringProp name="ThreadGroup.delay">0</stringProp>
</ThreadGroup>
<hashTree>
<JavaSampler guiclass="JavaTestSamplerGui" testclass="JavaSampler" testname="JavaTest no-op" enabled="true">
<elementProp name="arguments" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" enabled="true">
<collectionProp name="Arguments.arguments">
<elementProp name="Sleep_Time" elementType="Argument">
<stringProp name="Argument.name">Sleep_Time</stringProp>
<stringProp name="Argument.value">${__P(sleep,0)}</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
<elementProp name="Sleep_Mask" elementType="Argument">
<stringProp name="Argument.name">Sleep_Mask</stringProp>
<stringProp name="Argument.value">0</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
</collectionProp>
</elementProp>
<stringProp name="classname">org.apache.jmeter.protocol.java.test.JavaTest</stringProp>
</JavaSampler>
<hashTree/>
</hashTree>
Saved as generator-ceiling.jmx (inside a Test Plan with Functional Test Mode off), it was run three times on the
same 8-core laptop for 20 s with 8 threads: with CSV results, with XML results, and with no results file at all.
jmeter -n -t generator-ceiling.jmx -l ceiling.jtl -f -Jthreads=8 -Jduration=20 -Jsummariser.interval=10
jmeter -n -t generator-ceiling.jmx -l ceiling.xml -f -Jthreads=8 -Jduration=20 -Jsummariser.interval=10 \
-Jjmeter.save.saveservice.output_format=xml
jmeter -n -t generator-ceiling.jmx -Jthreads=8 -Jduration=20 -Jsummariser.interval=10
The final summariser line of each run (the comment lines are added here to label the runs):
# CSV results
summary = 5462042 in 00:00:20 = 272665.8/s Avg: 0 Min: 0 Max: 3 Err: 0 (0.00%)
# XML results
summary = 788943 in 00:00:20 = 39342.9/s Avg: 0 Min: 0 Max: 2 Err: 0 (0.00%)
# no results file
summary = 10758413 in 00:00:20 = 536178.1/s Avg: 0 Min: 0 Max: 84 Err: 0 (0.00%)
| Run | Samples per second | Results file | Bytes per sample on disk |
|---|---|---|---|
No results file |
536,178 |
none |
0 |
CSV results (default) |
272,666 |
431 MB for 5.46 million rows |
about 79 |
XML results |
39,343 |
124 MB for 0.79 million samples |
about 157 |
Read the numbers as ratios, not as a promise for any other machine: on this platform, writing CSV results halved the ceiling and writing XML cut it by a further factor of about seven. A real HTTP sample costs far more than a no-op (a socket round trip, parsing, extractors), so the HTTP ceiling is much lower. The Bookshelf load profile needs about 15.6 requests per second at 50 threads, four orders of magnitude below this ceiling; the experiment is useful the other way round, for plans that aim at thousands of requests per second, where the results format and per-sample work decide whether one generator suffices. Run it on each new generator type and keep the figure with the machine’s description.
Delay thread creation until needed. The manual notes that this Thread Group option lets a plan contain a very large total number of threads, provided that not too many are active at once. It saves memory when threads are short-lived and staggered; it does not raise the number of threads that can run concurrently.
16.3 and 16.4 Cookie Manager and Authorization Manager placement
The chapter only points to the web test tutorials. The rules that matter for load: one Cookie Manager, one Cache Manager and one Authorization Manager per scope (only one of each is used), placed at Thread Group level so every sampler of a virtual user shares them; Header Managers are merged in 5.6.3, with the one closest to the sampler winning on duplicates. Whether cookies are cleared per iteration depends on the Thread Group’s Same user on each iteration when the Cookie Manager’s Use Thread Group configuration is selected. All of it is on HTTP Sessions, Headers, Cookies & Authentication.
16.5 The HTTP(S) Test Script Recorder
The single most important recorder setting, in the manual’s words, is to filter out every request you are not
interested in. Either include only what matters (the manual’s example is .*\.jsp as an include pattern) or exclude
static resources (.*\.gif as an exclude pattern); JMeter can download embedded resources itself at run time, so
recording them only clutters the plan. For a JSON API such as Bookshelf, include the API paths (for example
.*/api/.*) and exclude everything else.
Two more points from the chapter: user-defined variables defined before recording let the recorder replace recorded
values (a host name becomes ${host}), and that matching is case-sensitive; if nothing is recorded, the browser is
probably not using the proxy (some browsers bypass it for localhost). The full recorder, the certificate and the clean-up
of a recording are on Recording & cURL Import.
16.6 User variables
The manual’s recipe, unchanged in substance since the books: a comma-separated file of user names and passwords next
to the plan, a CSV Data Set Config that names the columns, and samplers that reference the variables. The CSV Data Set
reads a new line for each thread. In the Bookshelf plan the file is users.csv (placeholder values) and the variables
are username and password, referenced as ${username} and ${password} in the login request. Sharing modes,
end-of-file behaviour, how many rows a run needs and distributing the file to every node are on
Parameterization & Test Data.
16.7 Reducing resource requirements
This is the item with the most direct effect on results. Each suggestion, why it saves resources, and how to check that a plan follows it:
| Suggestion | Why | How to check |
|---|---|---|
Use CLI mode ( |
The GUI renders results and keeps them in memory; the manual says GUI mode should only be used to create the script. |
The run command starts with |
Use as few listeners as possible; with |
Most listeners keep a copy of every sample, even in CLI mode. View Results Tree and View Results in Table are for the scripting phase only. |
|
Use the same sampler in a loop with variables (CSV Data Set) instead of many similar samplers |
Every element is cloned per thread; a plan with hundreds of near-identical samplers multiplies memory. The manual adds that an Include Controller does not help, because it adds all elements of the included file to the plan. |
Count samplers per Thread Group; repeated requests differing only in data are a smell. |
Do not use functional mode |
Functional Test Mode on the Test Plan saves response data and sampler data for every sample. |
|
Use CSV output rather than XML |
Measured above: XML results cut the sample ceiling by a factor of about seven against CSV on the same machine. |
|
Only save the data that you need |
Every saved field costs CPU and disk per sample. Response data, sampler data and headers are off by default; keep them off, and use the save-on-error option when bodies are needed for diagnosis. |
Review |
Use as few assertions as possible |
Each assertion runs on every sample it scopes; regular expressions and XPath on large bodies are the expensive ones. |
One cheap assertion per business-relevant sampler; see Assertions. |
Use the most performing scripting language |
Groovy with compiled-script caching; BeanShell and JavaScript are interpreted on each call. |
|
Create large or random test data in a file read by the CSV Data Set |
Generating data at run time costs CPU on every iteration and competes with the load. |
Data is generated before the run; see Parameterization & Test Data. |
An automated plan review. Most of the table can be checked mechanically on the .jmx file before a run. The script
below is a line-based check written for this section, not an official tool: it flags GUI listeners that keep samples,
BeanShell and BSF elements, JSR223 elements not in Groovy or without the compiled-script cache, Functional Test Mode,
the deprecated MongoDB elements and host names written into samplers instead of a property. It cannot see that an
element is disabled through a disabled parent, so treat a finding as "look at this line".
#!/usr/bin/env bash
# plan-review.sh: flag load-unfriendly elements in a .jmx before a big run.
# A line-based check: it sees elements switched off with enabled="false" on
# their own line, but not children of a disabled parent.
plan="${1:?usage: plan-review.sh plan.jmx}"
status=0
check() {
local hits
hits=$(grep -nE "$2" "$plan" | grep -v 'enabled="false"')
if [ -n "$hits" ]; then
echo "WARN $1"
echo "$hits" | sed -E 's/^([0-9]+):[[:space:]]*/ line \1: /' | cut -c1-110
status=1
fi
}
check "GUI listener that keeps samples in memory" \
'guiclass="(ViewResultsFullVisualizer|TableVisualizer|GraphVisualizer|AssertionVisualizer)"'
check "BeanShell or BSF element (migrate to JSR223 + Groovy)" 'testclass="(BeanShell|BSF)[A-Za-z]*"'
check "JSR223 element not using Groovy" \
'<stringProp name="scriptLanguage">(beanshell|bsh|javascript|ecmascript|nashorn|jexl)'
check "JSR223 compiled-script cache switched off" '<stringProp name="cacheKey">false</stringProp>'
check "Functional Test Mode switched on" '<boolProp name="TestPlan.functional_mode">true</boolProp>'
check "Deprecated MongoDB element" 'MongoScriptSampler|MongoSourceElement'
check "Host name written into a sampler (use a property)" \
'<stringProp name="HTTPSampler.domain">[A-Za-z0-9.-]+\.[A-Za-z]{2,}</stringProp>'
[ "$status" -eq 0 ] && echo "OK no findings in $plan"
exit "$status"
Run against two templates shipped in bin/templates of JMeter 5.6.3 and against the calibration plan above:
./plan-review.sh "$JMETER_HOME/bin/templates/build-web-test-plan.jmx"; echo "exit=$?"
./plan-review.sh "$JMETER_HOME/bin/templates/BeanShellSampler.jmx"; echo "exit=$?"
./plan-review.sh generator-ceiling.jmx; echo "exit=$?"
WARN GUI listener that keeps samples in memory
line 153: <ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="Vi
WARN Host name written into a sampler (use a property)
line 35: <stringProp name="HTTPSampler.domain">example.org</stringProp>
exit=1
WARN BeanShell or BSF element (migrate to JSR223 + Groovy)
line 4: <BeanShellSampler guiclass="BeanShellSamplerGui" testclass="BeanShellSampler" testname="BeanShel
exit=1
OK no findings in generator-ceiling.jmx
exit=0
The templates are fine for what they are, tutorials meant for the GUI; the findings show what to change before turning one into a load test. The non-zero exit status lets the script gate a CI job before the run starts.
16.8 The BeanShell server
The BeanShell interpreter can run as a server reachable by HTTP and telnet, and the manual is explicit that it has no
security: anyone who can connect to the port can issue any BeanShell command, with unrestricted access to JMeter and to
the host. It is off unless beanshell.server.port is set to a non-zero value; in the 5.6.3 jmeter.properties that line
is commented out, and when it is set the server listens on that port (HTTP) and the next one (telnet).
# bin/jmeter.properties in 5.6.3 (shipped defaults: the server is off)
#beanshell.server.port=9000
beanshell.server.file=../extras/startup.bsh
Source: Best Practices: BeanShell server
The practices that follow from it:
-
Keep it off on every generator. Check that no
user.properties,-Joption or profile file setsbeanshell.server.port:grep -rn beanshell.server.port "$JMETER_HOME/bin" profiles/should print only the commented default. -
If it is really needed, the manual’s condition is that the ports are protected against access, for example by a firewall. Bind the generator to a private network and block both ports from everywhere else.
-
The use case it was built for (changing a Constant Throughput Timer’s target in a running test by setting a property remotely) is better planned up front today: an Open Model Thread Group schedule or a Throughput Shaping Timer profile expresses the changing rate inside the plan, see Open Model & Throughput Control and Custom Thread Groups & Load Shaping.
The same reasoning applies to the RMI ports of distributed testing and to the UDP shutdown port, which is loopback-only; the security side of load generators is covered on Security & Responsible Load Testing.
16.9, 16.10 and 16.12 Scripting: JSR223 and Groovy
Three rules from the chapter, all detailed on Scripting with JSR223 & Groovy:
-
Use JSR223 elements with Groovy, and
__groovyinstead of__BeanShell(advised since 3.1). For intensive load the language’s engine should implement the JSR223Compilableinterface, which Groovy does and BeanShell and JavaScript do not in practice. -
Check Cache compiled script if available and never write
${varName}inside a cached script: the cache keeps the first value. Read variables withvars.get("varName")or pass them as Parameters. -
Develop functions as JSR223 Samplers first. Write and debug the script in a sampler with a View Results Tree, then store it in a Test Plan variable and call it as
${__groovy(${RANDOM_NAME})}; commas in the script need no escaping, because the function call is parsed before the variable is interpolated.
The BeanShell-specific advice of section 16.9 (one interpreter per element per thread, the Reset bsh.Interpreter before
each call option for long tests, the bsh.shared namespace) only matters for plans still being migrated.
16.11, 16.13 and 16.14 Parameters, shared data and properties
-
Parameterise with properties. One plan, many runs: the section’s plans read
threads,rampup,duration,hostandportwith${__P(threads,1)}and friends, set per run with-Jthreads=50or grouped in a profile file passed with-q profiles/load.properties. See Running Tests in CLI Mode. -
Share across threads with properties, not variables. Variables are per thread by design. A token obtained once in a setUp Thread Group is stored with
props.putand read with${__P(token)}orprops.get; see Properties, Variables & Functions. -
Never edit
jmeter.properties. Copy the property touser.propertiesand change it there;user.propertiesoverrides it and survives an upgrade. See Installation & Configuration.
16.15 Deprecated elements
Deprecated elements are hidden from the menu in version N and, the manual warns, removed in version N+1. In 5.6.3 they are
listed in the not_in_menu property of jmeter.properties:
# bin/jmeter.properties in 5.6.3
not_in_menu=org.apache.jmeter.protocol.mongodb.sampler.MongoScriptSampler,org.apache.jmeter.protocol.mongodb.config.MongoSourceElement,\
org.apache.jmeter.timers.BSFTimer,org.apache.jmeter.modifiers.BSFPreProcessor,org.apache.jmeter.extractor.BSFPostProcessor,org.apache.jmeter.assertions.BSFAssertion,\
org.apache.jmeter.visualizers.BSFListener,org.apache.jmeter.protocol.java.sampler.BSFSampler,\
org.apache.jmeter.protocol.http.control.gui.SoapSamplerGui
So the MongoDB Script sampler and MongoDB Source Config, every BSF element and the old SOAP sampler GUI are hidden. The
manual’s procedure to show one again for migration only is to copy not_in_menu into user.properties and remove that
element’s class name from the list. A plan that still uses one of them opens and runs, which is exactly the trap: the
next major release may not load it. The planned 6.0 removes the MongoDB elements; migrate them to JSR223 with the
MongoDB driver, BSF to JSR223 Groovy, and SOAP to an HTTP Request with an XML body, as described on
REST, JSON, GraphQL & SOAP APIs and
What’s Changed & Migration.
JVM tuning for the load generator
The launcher scripts already apply sensible JVM settings, and a dedicated generator needs only a few changes. The
variables themselves (HEAP, GC_ALGO, JVM_ARGS, JMETER_COMPLETE_ARGS, setenv.sh) are documented on
Installation & Configuration, and the
memory arithmetic (heap, metaspace, thread stacks) on
Running Tests in CLI Mode. This section gives
the decisions.
| Setting | Shipped in 5.6.3 bin/jmeter |
Recommendation for a load generator |
|---|---|---|
|
|
Size it from a calibration run with a GC log, keep |
|
|
Keep G1. The script’s pause goal is 100 ms; the Getting Started page lists 250 ms, and the script is what runs. Change the collector only after a GC log shows a problem that heap size does not fix. |
Heap dump on OOM |
|
Keep it; it costs nothing until triggered. Make sure the working directory has room for a file the size of the heap. |
GC log |
Off; a |
Turn it on for calibration runs and keep the log with the report. |
Extra options |
The script’s comments suggest |
Optional. Add them through |
Open-files soft limit |
Not set by the script; the JVM’s |
Raise the hard limit of the account or service; see the OS section below. |
A setenv.sh for a dedicated generator. The jmeter script sources bin/setenv.sh if it exists, so the
generator’s settings survive in one file and the command lines stay short. For a 4-vCPU, 8 GB Linux machine running the
Bookshelf load profile (the sizing is worked out on the CLI page):
# bin/setenv.sh on a dedicated 8 GB load generator
export HEAP="-Xms2g -Xmx2g -XX:MaxMetaspaceSize=256m"
# GC log per process for calibration runs; remove or keep, it is cheap
export JVM_ARGS="-Xlog:gc*:file=gc_jmeter_%p.log:time,uptime"
Verify what the JVM actually got. Environment variables are easy to lose in a CI job or a container entry point.
While a test runs, jcmd lists the final flags. On a run started with
HEAP="-Xms2g -Xmx2g -XX:MaxMetaspaceSize=256m" and the shipped GC_ALGO:
pid=$(jcmd | awk '/ApacheJMeter/ {print $1}')
jcmd "$pid" VM.flags | tr ' ' '\n' | grep -E \
'^-XX:(.UseG1GC|MaxGCPauseMillis|G1ReservePercent|InitialHeapSize|MaxHeapSize|MaxMetaspaceSize|.HeapDumpOnOutOfMemoryError)=?'
-XX:G1ReservePercent=20
-XX:+HeapDumpOnOutOfMemoryError
-XX:InitialHeapSize=2147483648
-XX:MaxGCPauseMillis=100
-XX:MaxHeapSize=2147483648
-XX:MaxMetaspaceSize=268435456
-XX:+UseG1GC
InitialHeapSize equal to MaxHeapSize confirms -Xms equals -Xmx, and MaxGCPauseMillis=100 confirms the
launcher’s GC_ALGO was applied.
Source: The jcmd Command (Java SE 21)
Other collectors. The Java 21 reference describes ZGC (-XX:+UseZGC) as a low-latency collector with pause
times of a few milliseconds independent of heap size, "at some throughput cost". A load generator is a throughput
workload whose pauses also matter, because a pause during a sample is added to its response time. G1 with a correctly
sized heap usually gives both; if a GC log shows long G1 pauses that a larger heap does not cure, ZGC is the documented
alternative to try, set through GC_ALGO and compared with a calibration run. Do not switch collectors because of a
blog post.
In containers, set HEAP explicitly to fit the container’s memory limit (for example 75 % of it), because the
launcher’s -Xmx overrides the JVM’s container-aware default (-XX:MaxRAMPercentage, 25 %). Tools such as free inside
a container show the host’s memory, not the limit; read /sys/fs/cgroup/memory.max instead, as the preflight script
below does.
OS tuning for the load generator
Three operating-system limits show up as errors that look like server failures: open files, local ports, and sockets waiting in TIME_WAIT. The CLI page explains each with worked numbers (file descriptors, ephemeral ports); this section covers making the settings persistent on each kind of host, and the myths to avoid.
Open files: soft limit, hard limit and the JVM
Every socket, results file, log and jar is a file descriptor. Two facts decide what limit JMeter really gets:
-
The JVM raises its own soft limit to the hard limit. The Java 21 reference documents
MaxFDLimitas enabled by default on all platforms (ignored on Windows): the JVM sets the soft limit for open files to the hard limit. On macOS that imposes a maximum of 10240, which the reference notes is lower than the system maximum. So on Linux the number that matters is the hard limit (ulimit -Hn), not the soft one a shell reports withulimit -n. -
Measured. A container started with a soft limit of 1024 and a hard limit of 65536 runs JMeter with 65536 for both:
# any Linux image with a JDK 21 and JMeter 5.6.3 on the PATH, the plan mounted in /work
docker run --rm --ulimit nofile=1024:65536 -v "$PWD:/work" -w /work jmeter-generator:5.6.3 bash -c '
echo "shell: soft $(ulimit -Sn), hard $(ulimit -Hn)"
jmeter -n -t generator-ceiling.jmx -Jthreads=2 -Jduration=8 -Jsleep=10 >/dev/null 2>&1 &
sleep 5; grep "Max open files" /proc/$(pgrep java)/limits'
shell: soft 1024, hard 65536
Max open files 65536 65536 files
Source: The java Command (Java SE 21), -XX:-MaxFDLimit and docker container run: --ulimit (Docker Docs)
Where to set the hard limit persistently, by how the generator is started:
| Started as | Setting | Notes |
|---|---|---|
An interactive login on Linux |
|
Applied by |
A systemd service (a CI runner, a JMeter server node) |
|
systemd’s own default, |
A Docker container |
|
Format |
macOS |
|
|
Windows |
Not applicable: the reference says |
The port limits below are the ones to watch. |
Source: limits.conf(5), systemd-system.conf(5) and systemd.exec(5)
Ephemeral ports and TIME_WAIT
Each outgoing connection takes a local port from the ephemeral range, and the side that closes a TCP connection first keeps it in TIME_WAIT for a while before the port can be reused towards the same destination. With keep-alive the Bookshelf profile holds about one connection per thread for the whole run and never comes near the limit; a plan that opens a new connection per request can exhaust the range within a minute, as the CLI page works out. The order of remedies is the same everywhere: reuse connections first, spread the load over more generators or source addresses second, and widen the port range third.
| Platform | Inspect | Change persistently |
|---|---|---|
Linux |
|
A file in |
Docker |
|
|
Kubernetes |
The same file inside the pod |
|
macOS |
|
|
Windows |
|
|
# /etc/sysctl.d/90-loadgen.conf on a Linux load generator
net.ipv4.ip_local_port_range = 15000 64000
# Kubernetes pod running a JMeter generator: a safe sysctl, no cluster change needed
apiVersion: v1
kind: Pod
metadata:
name: jmeter-generator
spec:
securityContext:
sysctls:
- name: net.ipv4.ip_local_port_range
value: "15000 64000"
containers:
- name: jmeter
image: registry.example.internal/jmeter:5.6.3
args: ["-n", "-t", "/plans/bookshelf-load.jmx", "-q", "/plans/profiles/load.properties", "-l", "/results/load.jtl"]
Source: IP Sysctl (Linux kernel documentation), docker container run: --sysctl (Docker Docs), Using sysctls in a Kubernetes Cluster (Kubernetes) and The default dynamic port range for TCP/IP has changed (Microsoft Learn)
The image name in the pod is a placeholder: there is no official Apache JMeter image, and building one is covered on
CI/CD with Maven, Docker & GitHub Actions. Running
generators in Kubernetes also needs CPU and memory requests and limits that match HEAP; see
Resource Management.
Myths to avoid, each contradicted by the kernel documentation:
-
tcp_fin_timeoutdoes not shorten TIME_WAIT. It is the time an orphaned connection stays in FIN_WAIT_2 (default 60 s). The documentation lists no setting for the length of TIME_WAIT, which is the 60 s kernel constantTCP_TIMEWAIT_LEN. -
tcp_tw_recycleno longer exists. Old tuning guides recommend it; it is absent from the current ip-sysctl documentation and from current kernels. -
tcp_tw_reuse(default 2, reuse for loopback traffic only) can be set to 1 to reuse TIME_WAIT sockets for new outgoing connections when safe, but the documentation says it should not be changed without advice from technical experts. Prefer keep-alive. -
tcp_max_tw_bucketsmust not be lowered. The documentation says it exists only to prevent simple denial-of-service attacks and that it should be raised, not lowered, if needed. Lowering it destroys TIME_WAIT sockets early and prints warnings. -
TIME_WAIT is on the side that closes first. If the server closes connections (for example it answers with
Connection: close), the TIME_WAIT sockets accumulate on the server, not on the generator; the generator then sees new connections being opened at a high rate, which shows in the JTL’sConnectcolumn.
A preflight check on the generator
Before a large run, print the limits that the run will actually get, on each generator. The script reads /proc
directly, so it also works in slim container images without ss or lsof:
#!/usr/bin/env bash
# generator-preflight.sh: print the load generator's limits before a run (Linux).
echo "java : $(java -version 2>&1 | head -1)"
echo "jmeter : $(jmeter -v 2>/dev/null | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' | tail -1)"
echo "cpus : $(nproc)"
echo "memory : $(free -m | awk '/^Mem:/ {print $2 " MiB total, " $7 " MiB available"}')"
echo "cgroup mem : $(cat /sys/fs/cgroup/memory.max 2>/dev/null || echo n/a)"
echo "nofile : soft $(ulimit -Sn), hard $(ulimit -Hn)"
echo "port range : $(cat /proc/sys/net/ipv4/ip_local_port_range | tr '\t' ' ')"
echo "tcp_tw_reuse: $(cat /proc/sys/net/ipv4/tcp_tw_reuse)"
# state 06 in /proc/net/tcp is TIME_WAIT; works where ss is not installed
echo "time-wait : $(awk '$4 == "06"' /proc/net/tcp /proc/net/tcp6 2>/dev/null | wc -l | tr -d ' ') sockets"
Run in a Linux container with JDK 21 and JMeter 5.6.3, started with --ulimit nofile=65536:65536,
--sysctl net.ipv4.ip_local_port_range="15000 64000" and a 4 GiB memory limit:
java : openjdk version "21.0.12.1" 2026-08-18 LTS
jmeter : 5.6.3
cpus : 8
memory : 7834 MiB total, 7193 MiB available
cgroup mem : 4294967296
nofile : soft 65536, hard 65536
port range : 15000 64000
tcp_tw_reuse: 2
time-wait : 0 sockets
The memory line shows the host’s 7.8 GB while the container may use only 4 GiB (cgroup mem): size HEAP from the
second number. Keep the output with the run’s report, next to the generator’s CPU and GC metrics described on
Running Tests in CLI Mode.
Plan-review checklist before a big run
A large run costs hours of several people’s time and of a shared environment. Go through this list first; each line names how to verify it and where the details are. The order follows the layers above.
| # | Area | Check | How to verify | Details |
|---|---|---|---|---|
1 |
Versions |
Every generator runs the same JMeter (5.6.3), Java (17+, 21 recommended) and plugin versions. |
|
|
2 |
Plan |
The plan passed a 1-thread smoke run with a View Results Tree, and that listener is now disabled. |
Smoke JTL has no errors; |
|
3 |
Plan |
No BeanShell, BSF or deprecated elements; JSR223 in Groovy with the cache on and no |
|
|
4 |
Plan |
Functional Test Mode off; assertions few and cheap. |
|
|
5 |
Plan |
Host, port, threads, ramp-up and duration come from properties; the profile is a |
|
|
6 |
Plan |
Correlation works for every user, not just the first: tokens and IDs are extracted, and a failed extraction is visible
through the extractor’s default value ( |
Smoke run with several users; check that the dependent requests succeed. |
|
7 |
Plan |
Think time is present and realistic; Transaction Controllers set Include duration of timer and pre-post processors explicitly. |
Read the timers; the property is present in the |
|
8 |
Plan |
Cookie, Cache and Authorization Managers at Thread Group level, one of each; Same user on each iteration chosen on purpose; keep-alive on unless new connections are the point. |
Inspect the tree. |
|
9 |
Load model |
Closed or open model chosen for a reason; for a closed model, Little’s Law confirms there are enough threads for the target rate (Bookshelf: 50 threads, Z = 3 s, R ≈ 0.2 s gives ≈ 15.6 req/s). |
Calculation written in the test plan document. |
Performance Testing Fundamentals, Open Model & Throughput Control |
10 |
Test data |
Generated before the run, enough rows for the whole duration, right sharing mode, a copy on every generator. |
Row count against threads × iterations; files present on each node. |
|
11 |
Results |
CSV results, a new file per run ( |
|
|
12 |
Results |
APDEX thresholds and pass/fail criteria are written down before the run, and a baseline exists to compare against. |
Properties set; criteria in the plan document. |
|
13 |
JVM |
|
|
|
14 |
OS |
The hard open-files limit covers sockets plus about 150 other files with margin. |
Preflight |
This page, Open files |
15 |
OS |
The ephemeral port range covers the new-connection rate × 60 s of TIME_WAIT, or connections are reused. |
Preflight |
This page, Ephemeral ports and TIME_WAIT |
16 |
Generator |
The generator’s ceiling is known (JavaTest or calibration run) and the planned load keeps its CPU under about 80 %. |
Calibration figures recorded for the machine type. |
This page, section 16.2 |
17 |
Generator |
Bandwidth estimate (requests per second × average response size) is well under the link capacity. |
Smoke-run |
|
18 |
Monitoring |
Generator and server metrics are both recorded for the run, on clocks synchronised between machines. |
Dashboards open before the start; time source checked on each host. |
|
19 |
Security |
BeanShell server off; RMI ports of distributed runs not reachable from outside; no real credentials in files. |
|
|
20 |
Run |
The target is |
Announcement sent; command rehearsed in the smoke run. |
|
21 |
Run |
Logging at INFO (no debug categories left on), and the run log goes to a file with |
|
Rows 2 to 5 are largely automated by plan-review.sh and rows 1, 13 to 15 by the preflight script, so a CI pipeline can enforce
them; the others need a person.
The books' advice: still valid or outdated
The three books the section draws on are good on method and dated on tooling. The table sorts their advice that relates to running and tuning tests; each row names the book and chapter.
| Advice (book, chapter) | Status | Current practice |
|---|---|---|
Define acceptance criteria, take a baseline, change one thing at a time in a test-tune loop (PTwJ ch. 1) |
Valid |
Unchanged. Keep the baseline JTL and dashboard with the plan; see Performance Testing Fundamentals. |
Keep resource utilisation targets, around 80 % (Pro ch. 2) |
Valid |
Applies to the generator too: keep its CPU under about 80 % during the hold. |
Model the workload from access logs as a percentage mix with test data and mocked third parties (Pro ch. 11) |
Valid |
Implemented with Throughput, Switch and Random controllers and CSV data. |
Size thread groups and ramp-up from the target load (PTwJ ch. 2; Pro ch. 5) |
Valid, extended |
Same fields, plus Same user on each iteration, Delay thread creation and the scheduler; open-model alternatives since 5.5. |
Run load tests in non-GUI mode (Pro ch. 3) |
Valid, renamed |
Called CLI mode since 5.1; same |
Use the HttpClient3.1 implementation (Pro ch. 3, 5) |
Outdated |
Only |
Script with BSF (PTwJ ch. 3) or BeanShell pre- and post-processors (Pro ch. 3) |
Outdated |
JSR223 with Groovy and cached compilation; BSF is hidden as deprecated and BeanShell is discouraged. |
Extract JSON with the |
Outdated |
Core JSON Extractor, JMESPath Extractor and JSON Assertion: no plugin, less to install on each generator. |
Report with GUI listeners and plugin graphs (Cookbook ch. 7) |
Outdated |
GUI listeners are the first thing to remove for load; use the HTML dashboard (its 3.0 form is in Pro ch. 12). |
Monitor servers with the Monitor Results listener or PerfMon and ServerAgent (PTwJ ch. 5; Pro ch. 9) |
Outdated |
Monitor Results was removed in 3.2 and PerfMon is legacy; use the Backend Listener and the server’s own metrics. |
Step load with the Stepping Thread Group (Cookbook ch. 6) |
Outdated |
Concurrency Thread Group (plugin) or the core Open Model Thread Group. |
Record with the WorkBench (Pro ch. 4) |
Outdated |
WorkBench was removed in 4.0; use the Recording template and, per best practices, strict include and exclude filters. |
Run distributed master and slaves without SSL; choose a sample sender mode (Pro ch. 6; Cookbook ch. 5) |
Partly outdated |
RMI over SSL is the default since 4.0; StrippedBatch is the default sender; scaling out is still the answer to an undersized generator. |
Install plugins by unzipping the Standard and Extras sets (Cookbook appendix) |
Outdated |
Plugins Manager, from the GUI or |
Raise the heap by editing it in the launcher script; read logs for OutOfMemoryError (Pro ch. 8) |
Valid idea, new mechanism |
Set |
Throttle bandwidth with |
Valid |
Still in the properties reference; it simulates slow clients but does not reduce the generator’s own work. |
Integrate with the Jenkins Performance plugin and jmeter-maven-plugin 1.10 (Cookbook ch. 8; Pro ch. 7) |
Outdated |
jmeter-maven-plugin 3.8.x and CI workflows that gate on the results, not on the exit code. |
The detailed history of each change, and the full idiom-by-idiom migration table, are on What’s Changed & Migration.
Pitfalls
-
Tuning the kernel before the plan. An enabled View Results Tree or XML results cost more than any sysctl gains. Work through the layers in order.
-
Trusting
ulimit -nin a shell. The JVM uses the hard limit; a service started by systemd or a container runtime has its own limits. Check/proc/<pid>/limitsof the running JMeter. -
Trusting
freein a container. It shows the host; sizeHEAPfrom the cgroup limit. -
Lowering
tcp_fin_timeoutto fight TIME_WAIT. It does not affect TIME_WAIT. Reuse connections instead. -
Calibrating once on a laptop. The ceiling depends on the machine type; measure on the generator that will run the test and record it.
-
A deprecated element left in a plan that still runs today and fails to load after an upgrade.
-
Leaving the BeanShell server enabled on a generator reachable from a shared network: it is remote code execution by design.
-
Different versions across generators. Merged results from different JMeter or plugin versions are not one experiment.
From the books to 5.6 to 6.0
| Topic | Books (JMeter 2.11 to 3.0) | 5.6.3, and planned 6.0 |
|---|---|---|
Best-practices chapter |
BeanShell still common; JSR223 advice appears from 3.1 |
Same chapter, JSR223 and Groovy advised; the BeanShell server warning unchanged |
Scripting language |
BeanShell, BSF, JavaScript |
Groovy 3.0.20 bundled in 5.6.3; 6.0 plans Groovy 5 |
Deprecated elements |
MongoDB deprecated in 3.0; SOAP and Monitor Results still present |
|
Java |
Java 7 or 8 |
Java 8+ for 5.6.3 (17+ recommended); 6.0 plans to require Java 17 |
Heap and GC |
|
|
References
-
Best Practices (User Manual): sections 16.1 to 16.15
-
Getting Started (User Manual): configuring JMeter, command-line options
-
Apache JMeter source code: tag
rel/v5.6.3(bin/jmeter,bin/jmeter.properties,bin/templates) for the shipped launcher and property defaults;xdocs/changes.xmlon master for planned 6.0 changes -
The java Command (Java SE 21):
MaxFDLimit,MaxGCPauseMillis,UseZGC,MaxRAMPercentage,-Xlog -
IP Sysctl (Linux kernel documentation):
ip_local_port_range,tcp_tw_reuse,tcp_fin_timeout,tcp_max_tw_buckets -
Control Group v2 (Linux kernel documentation):
memory.max -
systemd-system.conf(5) and systemd.exec(5) (systemd manual sources)
-
docker container run (Docker Docs):
--ulimit,--sysctl -
The default dynamic port range for TCP/IP has changed (Microsoft Learn)
-
Bayo Erinle, JMeter Cookbook, Packt, 2014 (JMeter 2.11): ch. 2 (JSON extraction), ch. 5 (distributed testing), ch. 6 (Stepping and Ultimate thread groups), ch. 7 (reporting with listeners), ch. 8 (CI and bandwidth throttling), appendix (plugin installation); used for concepts only.
-
Bayo Erinle, Performance Testing with JMeter, 2nd ed., Packt, 2015 (JMeter 2.12): ch. 1 (methodology), ch. 2 (thread groups), ch. 3 (BSF), ch. 5 (monitoring), ch. 7 (JSON plugin); used for concepts only.
-
Sai Matam and Jagdeep Jain, Pro Apache JMeter, Apress, 2017 (JMeter 3.0): ch. 2 (utilisation targets), ch. 3 (non-GUI mode, BeanShell), ch. 4 (WorkBench recording), ch. 5 (thread groups, HTTP implementations), ch. 6 (distributed testing), ch. 7 (Maven), ch. 8 (troubleshooting and heap), ch. 9 (monitoring), ch. 10 (bandwidth), ch. 11 (workload modelling), ch. 12 (dashboard); used for concepts only, with every command taken from the current manual.