User-Defined Variables and Properties

8 min read

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:

NameValue
base_path/api/v1
timeout_ms5000
default_roleuser

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=500

The -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-xxxxx

Pass it at run time:

jmeter -n -t test.jmx -q env-prod.properties

This 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 time

For a production run:

jmeter -n -t test.jmx -l results.jtl \
  -JbaseUrl=https://prod.example.com \
  -JthinkTime=3000

For 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:

VariableValue
${__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 .jmx file. 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, use vars.put() in a Pre-Processor.
  • Confusing vars and props scope. vars.put() sets a thread-local variable — only the current thread sees it. props.put() sets a global property — all threads see it. Using vars.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.

  1. 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)} and api_version = v1.
  2. Update all HTTP Request Defaults (or the HTTP Requests directly) to use ${base_url} as the Server Name field.
  3. Run the test normally — confirm it still targets test.k6.io (the default).
  4. Run from CLI with -JbaseUrl=https://httpbin.org — confirm the test now targets httpbin.org without any changes to the .jmx file.
  5. Create a local.properties file with baseUrl=https://test.k6.io and thinkTime=1000. Run with jmeter -q local.properties -n -t test.jmx -l results.jtl. Confirm the properties file is picked up correctly.
  6. Add a JSR223 Pre-Processor to one sampler. Inside the script, write: vars.put("requestTime", new Date().toString()). Add ${requestTime} as a custom header X-Request-Initiated. Confirm in View Results Tree that the header appears with the correct timestamp.

// tip to track lessons you complete and pick up where you left off across devices.