JMeter has two distinct systems for defining values that can be referenced across a test plan: User-Defined Variables and Properties. They look similar on the surface — both use ${...} syntax — but they have different scopes, different persistence, and different intended use cases. Understanding which to reach for in a given situation prevents a class of subtle bugs that are hard to trace.
User-Defined Variables
User-Defined Variables (UDVs) are name-value pairs defined inside the test plan itself. Add them via right-click → Add → Config Element → User Defined Variables.
Each UDV element contains a table of name-value pairs:
| Name | Value |
|---|---|
base_path | /api/v1 |
timeout_ms | 5000 |
default_role | user |
Reference them anywhere in the test plan with ${variableName}:
HTTP Request path: ${base_path}/users
HTTP Header: X-Timeout: ${timeout_ms}
UDVs defined at Test Plan root level are initialised once at the start of the test and are available to all Thread Groups. UDVs defined at Thread Group level are initialised per-thread when that thread starts.
Important: UDVs are initialised before threads start. You cannot set a UDV dynamically during a test run from a script and have it affect the initial value — use vars.put() from a JSR223 script for runtime variable mutation, or Properties for cross-thread communication.
JMeter Properties
Properties are key-value pairs that exist at the JMeter engine level — shared across all threads and accessible from any test plan element. They are the standard mechanism for parameterising tests from outside the .jmx file.
Reading a property:
${__P(propertyName,defaultValue)}
The second argument is the default — returned if the property is not set. Always provide a default so the test plan is self-contained and runnable without flags.
Setting a property at runtime from CLI:
jmeter -n -t test.jmx -JbaseUrl=https://prod.example.com -JmaxUsers=500The -J prefix sets a property that the test plan can read with ${__P(baseUrl,https://staging.example.com)}.
Setting properties in a file:
Create env-prod.properties:
baseUrl=https://prod.example.com
dbHost=db-prod.internal
apiKey=sk-prod-xxxxxPass it at run time:
jmeter -n -t test.jmx -q env-prod.propertiesThis pattern gives you environment-specific configuration files committed to source control (without secrets) and secret values injected at CI/CD runtime via environment variables or vault systems.
User-Defined Variables vs Properties
User-Defined Variables
Defined inside the .jmx file
Scope: Test Plan or Thread Group
Reference: ${variableName}
Best for: static defaults in the plan
Set at initialisation — not runtime-overridable from CLI
Properties
Defined outside the .jmx file
Scope: global — all threads
Reference: ${__P(name,default)}
Best for: environment overrides from CI/CD
Set via -J flags, -q files, or jmeter.properties
A practical parameterisation pattern
The recommended approach for multi-environment test plans combines both systems:
In the .jmx file (User-Defined Variables at Test Plan level):
base_url = ${__P(baseUrl,https://staging.example.com)}
api_version = v2
think_time = ${__P(thinkTime,2000)}
The UDVs read their values from Properties at startup — Properties win if set, defaults apply otherwise. Samplers reference the UDV names ${base_url} and ${think_time} — they never reference __P directly, keeping the sampler configuration clean.
For a staging run (no flags needed):
jmeter -n -t test.jmx -l results.jtl
# Uses defaults: staging URL, 2000ms think timeFor a production run:
jmeter -n -t test.jmx -l results.jtl \
-JbaseUrl=https://prod.example.com \
-JthinkTime=3000For a CI/CD pipeline:
- name: Run JMeter load test
run: |
jmeter -n -t test.jmx -l results.jtl \
-JbaseUrl=${{ vars.TARGET_URL }} \
-JthinkTime=${{ vars.THINK_TIME_MS }}Runtime variable mutation with vars
Inside JSR223 Pre/Post-Processor scripts, vars is the thread-local variable map — the same map that CSV Data Set Config and UDVs write to.
// Read a variable
String userId = vars.get("userId")
// Set a variable for use in subsequent samplers
vars.put("authToken", responseToken)
vars.put("loginTime", String.valueOf(System.currentTimeMillis()))Variables set with vars.put() are thread-local — other threads cannot read them. For cross-thread communication, use props.put() and props.get() (the Properties map), which is shared globally.
Built-in variables
JMeter provides several variables without any configuration:
| Variable | Value |
|---|---|
${__threadNum} | Current thread number within the Thread Group (1-based) |
${__jmeterVersion()} | Running JMeter version |
${__time(HH:mm:ss,)} | Current time formatted |
${__machineIP()} | IP address of the machine running JMeter |
${__machineName()} | Hostname of the machine |
${__threadNum} is particularly useful for partitioning test data without CSV: user_${__threadNum}@test.com generates a unique email per thread without any external file.
⚠️ Common mistakes
- Putting environment-specific values in User-Defined Variables. If the staging URL is hardcoded as a UDV, switching to production means editing the
.jmxfile. This is fragile and error-prone — one forgotten edit runs a production load test against staging. Use Properties for anything environment-specific. - Expecting UDVs to be mutable across iterations. UDVs initialise once when the thread starts. If you change a UDV via a JSR223 script using
vars.put(), the new value is available for the rest of that thread's lifetime — but the UDV defined in the Config Element always wins on the next thread start. For truly dynamic, cross-iteration state, usevars.put()in a Pre-Processor. - Confusing
varsandpropsscope.vars.put()sets a thread-local variable — only the current thread sees it.props.put()sets a global property — all threads see it. Usingvars.put()when you intend cross-thread communication silently fails: other threads get an empty variable.
🎯 Practice task
Parameterise your test plan for multi-environment execution.
- Open your test plan. Add a User-Defined Variables config element at Test Plan level. Add two variables:
base_url = ${__P(baseUrl,https://test.k6.io)}andapi_version = v1. - Update all HTTP Request Defaults (or the HTTP Requests directly) to use
${base_url}as the Server Name field. - Run the test normally — confirm it still targets
test.k6.io(the default). - Run from CLI with
-JbaseUrl=https://httpbin.org— confirm the test now targetshttpbin.orgwithout any changes to the.jmxfile. - Create a
local.propertiesfile withbaseUrl=https://test.k6.ioandthinkTime=1000. Run withjmeter -q local.properties -n -t test.jmx -l results.jtl. Confirm the properties file is picked up correctly. - Add a JSR223 Pre-Processor to one sampler. Inside the script, write:
vars.put("requestTime", new Date().toString()). Add${requestTime}as a custom headerX-Request-Initiated. Confirm in View Results Tree that the header appears with the correct timestamp.