Logic Controllers — If, While, Loop, Switch

9 min read

Samplers send requests. Logic Controllers decide which requests get sent, in what order, and how many times. Without logic controllers, every JMeter thread executes the same sequence of samplers in the same order on every iteration. With logic controllers, you can branch on conditions, weight traffic across scenarios, loop specific sections, and group requests into measurable transactions — all without writing a line of code.

The controller model

Logic controllers work as containers. You add samplers (and other elements) inside them, and the controller decides whether and how to execute its children. Right-click any logic controller to add child samplers.

The tree structure in JMeter's GUI directly represents the controller hierarchy:

Thread Group
├── HTTP Request — "GET /homepage"        ← always runs
├── If Controller (condition: user is logged in)
│   └── HTTP Request — "GET /dashboard"   ← runs only when condition is true
└── Loop Controller (5 loops)
    └── HTTP Request — "GET /products"    ← runs 5 times per iteration

Loop Controller

Runs its children N times within a single thread iteration.

When to use it: repeating a specific action (polling, paginating, adding N items to a cart) without increasing thread count or the Thread Group's loop count.

Thread Group (1 user, 1 loop)
└── HTTP Request — "GET /login"
    Loop Controller (count: 10)
    └── HTTP Request — "GET /product/${productId}"
    HTTP Request — "POST /checkout"

This simulates a user who logs in once, browses 10 products, then checks out — all in one Thread Group iteration. The Loop Controller's loop count is independent of the Thread Group's loop count.

If Controller

Runs its children only when a condition evaluates to true. The condition is a Groovy expression that returns true or false.

Condition: ${__groovy("${response_code}" == "200")}

Reference any JMeter variable in the condition. The __groovy function evaluates the expression as Groovy code and returns the boolean result.

Common patterns:

# Run follow-up only if login succeeded
${__groovy("${login_status}" == "200")}

# Run only for admin users
${__groovy("${user_role}" == "admin")}

# Run on even iterations only
${__groovy(${__counter(FALSE,)} % 2 == 0)}

The Interpret Condition as Variable Expression checkbox changes evaluation mode — when checked, the condition is a simple variable reference that must contain true or false as a string, without the __groovy wrapper. The Groovy mode is more powerful.

While Controller

Runs its children repeatedly while a condition is true. Use it for polling — wait until a status endpoint returns a specific value.

While Controller — condition: ${__groovy("${job_status}" != "completed")}
├── HTTP Request — "GET /api/jobs/${jobId}/status"
│   └── JSON Extractor: extract $.status → job_status
└── Constant Timer — 2000ms (wait before polling again)

This simulates a user who submits a job and then polls every 2 seconds until it completes. The While Controller exits as soon as the Groovy condition returns false.

Set a sensible exit condition — if the condition never becomes false (for example, the API never returns completed), the While Controller runs forever, blocking that thread.

Switch Controller

Routes execution to exactly one child based on a switch value. The switch value is compared against each child controller's or sampler's name.

Switch Controller — Switch Value: ${user_type}
├── HTTP Request — (name: "admin") → admin-specific flow
├── HTTP Request — (name: "member") → member flow
└── HTTP Request — (name: "guest") → guest flow

If ${user_type} is "admin", only the sampler named "admin" runs. If no match is found, the first child runs as a default.

Switch Controller is cleaner than nested If Controllers when routing between three or more exclusive paths.

Throughput Controller

Runs its children a specified percentage of the time — or a fixed number of times per test run.

Percentage mode: set to 30%, and 30% of thread iterations execute the children. The remaining 70% skip them.

Total executions mode: set to 100, and the children run exactly 100 times across all threads for the entire test, regardless of total iterations.

Logic Controllers — when to use each

Loop Controller

  • Repeat a block N times per iteration

  • Browse N products before checkout

  • Poll an endpoint N times

  • Independent of Thread Group loop count

If Controller

  • Conditional execution via Groovy

  • Run follow-ups only on HTTP 200

  • Route by extracted user role

  • Skip flows that don't apply to this user

Throughput Controller

  • Weighted scenario mixing

  • 60% browse, 30% search, 10% checkout

  • Percentage or fixed count mode

  • Simulates realistic user distribution

Transaction Controller

  • Groups samplers as a named transaction

  • Reports aggregate timing for the group

  • Measures end-to-end flow duration

  • Appears as single entry in Aggregate Report

Transaction Controller

The Transaction Controller groups a set of samplers and reports their combined execution time as a single transaction. It does not control which samplers run — it is a measurement wrapper.

Transaction Controller — "User Login Flow"
├── HTTP Request — "GET /login-page"
├── HTTP Request — "POST /api/auth/login"
└── HTTP Request — "GET /api/user/profile"

In the Aggregate Report, "User Login Flow" appears as a single row with the sum of all three request times. This is how you measure end-to-end SLAs: "the login flow must complete in under 3 seconds" is a Transaction Controller assertion, not a per-sampler assertion.

Check Generate parent sample to create a parent result entry in the JTL file. Check Include duration of timer and pre-post processors in generated sample to include think time in the transaction duration measurement.

Once Only Controller

Runs its children exactly once per thread, on the first iteration only. Every subsequent iteration skips it.

Canonical use case: login. A user logs in once and reuses the session across all iterations.

Thread Group (10 users, 10 loops)
├── Once Only Controller
│   └── HTTP Request — "POST /api/auth/login"  ← runs once per VU
└── HTTP Request — "GET /api/dashboard"         ← runs 10 times per VU

Without Once Only Controller, the login request runs on every iteration — 10 × 10 = 100 login requests for a 10-user, 10-loop test. With it: 10 login requests (once per user) and 100 dashboard requests.

Module Controller and Include Controller

Module Controller references a Test Fragment defined elsewhere in the test plan. Test Fragments are reusable blocks of samplers that do not execute on their own — they are only executed when referenced by a Module Controller. Useful for extracting common flows (authentication, teardown) into one definition and reusing them from multiple Thread Groups.

Include Controller loads an external .jmx file at runtime and executes its contents. This enables modular test plan composition — maintain separate .jmx files for each user flow and compose them in a master test plan.

⚠️ Common mistakes

  • Infinite While Controller with no exit condition. A While Controller whose condition never becomes false runs forever, holding one thread hostage for the rest of the test. Always set a maximum iteration guard: ${__groovy("${job_status}" != "completed" && ${__counter(FALSE,)} < 20)} — the counter ensures the loop exits after 20 iterations regardless.
  • Using Loop Controller to set the Thread Group loop count. Loop Controller and Thread Group's Loop Count are different things. Loop Controller repeats a subset of the sampler tree; Thread Group's Loop Count repeats the entire sampler tree. Nesting a Loop Controller inside a Thread Group multiplies iterations: 5-loop Thread Group × 3-loop Loop Controller = 15 executions of the inner sampler.
  • Forgetting Transaction Controller only measures — it does not control. All samplers inside a Transaction Controller always execute. If you want conditional execution, use an If Controller inside the Transaction Controller, not the Transaction Controller itself as a conditional gate.

🎯 Practice task

Build a test plan that uses three different logic controllers.

  1. Start from first-test.jmx. Add a Loop Controller with count 3 around the product page request. Run with 1 user and confirm the product request appears 3 times in View Results Tree.
  2. Add a Once Only Controller containing a "POST /api/auth/login" request (targeting test.k6.io/auth/token/login/ with credentials). Set Thread Group to 1 user, 5 loops. Confirm the login request appears only once in results (not 5 times).
  3. Add a Transaction Controller named "Browse and Buy" wrapping two samplers. Check "Generate parent sample". Run and confirm "Browse and Buy" appears as a single entry in the Summary Report listener, alongside the individual sampler entries.
  4. Add a Throughput Controller in percentage mode set to 50%. Put an HTTP request inside it. Run with 1 user, 10 loops. Count how many times the inner request ran — it should be approximately 5 (50% of 10 iterations).

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