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.

flowchart LR A["Test plan: listeners, assertions, scripts, data"] --> B["JMeter settings: user.properties, results format"] B --> C["JVM: HEAP, GC_ALGO, thread stacks"] C --> D["Operating system: open files, ports, TIME_WAIT"] D --> E["Network: bandwidth, latency"] E --> F["System under test"]

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.

Installation & Configuration

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.

Thread Groups: Concurrent Users & Ramp-up

16.3, 16.4

Where to put the Cookie Manager and the Authorization Manager

Pointers to the web test tutorials.

HTTP Sessions, Headers, Cookies & Authentication

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 unknown_ca error.

Recording & cURL Import

16.6

User variables

Per-user logins from a CSV file read by a CSV Data Set Config, one line per thread.

Parameterization & Test Data

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 __groovy instead of BeanShell and __BeanShell; Groovy implements Compilable; cache compiled scripts and read variables with vars.get instead of ${varName}.

Scripting with JSR223 & Groovy

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 ${__groovy(${RANDOM_NAME})}.

Properties, Variables & Functions

16.11

Parameterising tests

Define Test Plan variables from properties with ${__P(loops,10)}, set them with -J, group them in files passed with -q.

Running Tests in CLI Mode

16.13

Sharing variables between threads and thread groups

Variables are per thread by design; use properties, files, or your own classes.

Properties, Variables & Functions

16.14

Managing properties

Never edit jmeter.properties; copy the property into user.properties, which overrides it.

Installation & Configuration

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

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.

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 (jmeter -n -t test.jmx -l test.jtl)

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 -n. Never size a generator from a GUI run.

Use as few listeners as possible; with -l they can all be deleted or disabled

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.

plan-review.sh below; see Listeners & Results Files: Memory cost of each listener.

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.

TestPlan.functional_mode is false in the .jmx.

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.

jmeter.save.saveservice.output_format is csv (the default) and no listener overrides it.

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 jmeter.save.saveservice.* in user.properties; see Listeners & Results Files.

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.

plan-review.sh below.

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

The practices that follow from it:

  • Keep it off on every generator. Check that no user.properties, -J option or profile file sets beanshell.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:

  1. Use JSR223 elements with Groovy, and __groovy instead of __BeanShell (advised since 3.1). For intensive load the language’s engine should implement the JSR223 Compilable interface, which Groovy does and BeanShell and JavaScript do not in practice.

  2. Check Cache compiled script if available and never write ${varName} inside a cached script: the cache keeps the first value. Read variables with vars.get("varName") or pass them as Parameters.

  3. 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, host and port with ${__P(threads,1)} and friends, set per run with -Jthreads=50 or 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.put and read with ${__P(token)} or props.get; see Properties, Variables & Functions.

  • Never edit jmeter.properties. Copy the property to user.properties and change it there; user.properties overrides 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

HEAP

-Xms1g -Xmx1g -XX:MaxMetaspaceSize=256m

Size it from a calibration run with a GC log, keep -Xms equal to -Xmx, and leave room for thread stacks, metaspace and the operating system. A heap larger than free RAM makes the generator swap.

GC_ALGO

-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:G1ReservePercent=20

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

-XX:+HeapDumpOnOutOfMemoryError, always added

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 VERBOSE_GC line for Java 9+ is commented out in the script

Turn it on for calibration runs and keep the log with the report.

Extra options

The script’s comments suggest -XX:+ParallelRefProcEnabled -XX:+PerfDisableSharedMem as optional

Optional. Add them through JVM_ARGS if wanted; neither changes results measurably for typical plans.

Open-files soft limit

Not set by the script; the JVM’s MaxFDLimit option, on by default, raises the soft limit to the hard limit

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.

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 MaxFDLimit as 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 with ulimit -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

Where to set the hard limit persistently, by how the generator is started:

Started as Setting Notes

An interactive login on Linux

/etc/security/limits.conf or a file in /etc/security/limits.d/, lines of the form domain type item value, for example loadtest - nofile 65536 (- sets soft and hard)

Applied by pam_limits to login sessions only, not to systemd services.

A systemd service (a CI runner, a JMeter server node)

LimitNOFILE=65536 in the unit’s [Service] section

systemd’s own default, DefaultLimitNOFILE=, is 1024 soft and 524288 hard; the soft limit stays 1024 because select() cannot handle descriptors above 1023, and programs that do not use it are expected to raise it themselves, which the JVM does.

A Docker container

docker run --ulimit nofile=65536:65536 (or ulimits: in Compose)

Format type=soft[:hard].

macOS

ulimit -n in the shell; the JVM caps at 10240 unless started with -XX:-MaxFDLimit

kern.maxfilesperproc is the system per-process maximum.

Windows

Not applicable: the reference says MaxFDLimit is ignored on Windows

The port limits below are the ones to watch.

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

sysctl net.ipv4.ip_local_port_range (default 32768 to 60999)

A file in /etc/sysctl.d/, for example net.ipv4.ip_local_port_range = 15000 64000, applied with sysctl --system. The kernel documentation asks for the two numbers to have different parity if possible.

Docker

cat /proc/sys/net/ipv4/ip_local_port_range inside the container

docker run --sysctl net.ipv4.ip_local_port_range="15000 64000": net.* sysctls are namespaced and allowed, except with --network=host, where the host’s value applies.

Kubernetes

The same file inside the pod

net.ipv4.ip_local_port_range is on the list of safe sysctls, settable in the pod’s securityContext.sysctls without cluster-wide changes.

macOS

sysctl net.inet.ip.portrange.first net.inet.ip.portrange.last

sudo sysctl -w for the session; a current macOS installation reported 49152 to 65535.

Windows

netsh int ipv4 show dynamicport tcp (default start 49152, 16,384 ports)

netsh int ipv4 set dynamic tcp start=10000 num=50000

# /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"]

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_timeout does 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 constant TCP_TIMEWAIT_LEN.

  • tcp_tw_recycle no 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_buckets must 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’s Connect column.

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.

jmeter -v, java -version, the preflight script; PluginsManagerCMD.sh status where plugins are used.

Installation & Configuration, Plugins & Plugins Manager

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; plan-review.sh reports no GUI listener.

GUI & Test Plan Workflow

3

Plan

No BeanShell, BSF or deprecated elements; JSR223 in Groovy with the cache on and no ${...} in scripts.

plan-review.sh.

Scripting with JSR223 & Groovy

4

Plan

Functional Test Mode off; assertions few and cheap.

plan-review.sh; review each assertion’s scope.

Assertions

5

Plan

Host, port, threads, ramp-up and duration come from properties; the profile is a -q file under version control.

grep -c '__P(' bookshelf-load.jmx; no literal host in samplers.

Running Tests in CLI Mode

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 (TOKEN_NOT_FOUND in the Bookshelf plan), which makes the next request fail.

Smoke run with several users; check that the dependent requests succeed.

Correlation & Extractors

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

Timers & Think Time, Logic Controllers & Transactions

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.

HTTP Sessions, Headers, Cookies & Authentication

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.

Parameterization & Test Data

11

Results

CSV results, a new file per run (-f or a unique name), response data saved only on error, the fields the dashboard needs.

user.properties and the command line.

Listeners & Results Files, HTML Dashboard Report

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.

Analyzing Results & Finding Bottlenecks

13

JVM

HEAP sized from a calibration run, -Xms = -Xmx, below available memory (or the cgroup limit); GC log on.

jcmd <pid> VM.flags; preflight script.

Running Tests in CLI Mode

14

OS

The hard open-files limit covers sockets plus about 150 other files with margin.

Preflight nofile line; /proc/<pid>/limits during the run.

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 port range and time-wait lines during the calibration run.

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 bytes column.

Running Tests in CLI Mode

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.

Real-time Monitoring with Backend Listener

19

Security

BeanShell server off; RMI ports of distributed runs not reachable from outside; no real credentials in files.

grep beanshell.server.port; firewall rules.

Security & Responsible Load Testing

20

Run

The target is localhost or a system you own, its owners know the time window, and the stop procedure is known (shutdown.sh, port 4445 on the generator).

Announcement sent; command rehearsed in the smoke run.

Running Tests in CLI Mode

21

Run

Logging at INFO (no debug categories left on), and the run log goes to a file with -j.

log4j2.xml and the command line.

Troubleshooting & Debugging

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 -n option, plus -e -o for the dashboard.

Use the HttpClient3.1 implementation (Pro ch. 3, 5)

Outdated

Only HttpClient4 and Java remain.

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 jp@gc JSON PathExtractor plugin (Cookbook ch. 2; PTwJ ch. 7)

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 PluginsManagerCMD, with pinned versions on every generator.

Raise the heap by editing it in the launcher script; read logs for OutOfMemoryError (Pro ch. 8)

Valid idea, new mechanism

Set HEAP through the environment or bin/setenv.sh; heap dumps on OOM are automatic; add a GC log.

Throttle bandwidth with httpclient.socket.http.cps (Cookbook ch. 8; Pro ch. 10)

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 -n in a shell. The JVM uses the hard limit; a service started by systemd or a container runtime has its own limits. Check /proc/<pid>/limits of the running JMeter.

  • Trusting free in a container. It shows the host; size HEAP from the cgroup limit.

  • Lowering tcp_fin_timeout to 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

not_in_menu hides MongoDB, BSF and the SOAP sampler GUI; 6.0 plans to remove MongoDB

Java

Java 7 or 8

Java 8+ for 5.6.3 (17+ recommended); 6.0 plans to require Java 17

Heap and GC

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

HEAP and GC_ALGO set through the environment or setenv.sh; G1 by default; heap dump on OOM always on

References