JMeter logic controllers decide which samplers run, in what order and how many times. They send no requests themselves: an If Controller runs its children only when a condition is true, a Throughput Controller runs them for a share of iterations, a Loop Controller repeats them. Only the Transaction Controller adds something to the results: a sample timing everything inside it.
We put every controller you are likely to use into one test plan, ran it in JMeter 5.6.3 with 4 threads for 10 iterations each, and counted the samples each one produced. Every count matched the component reference except one, the Transaction Controller’s parent mode, and the If Controller’s condition mode made the biggest practical difference: a 20,000-iteration run took 0.24 seconds with a Groovy condition and 5.4 seconds in JavaScript mode.
Key Takeaways
- Logic controllers sit inside a thread group and shape the flow of its samplers. JMeter 5.6 ships 17 of them.
- The If Controller should use “Interpret Condition as Variable Expression” with
__groovyor__jexl3. Iterations with its JavaScript mode took 22 times longer than with Groovy in our run. - The Throughput Controller, badly named by the manual’s own account, runs its children for a percentage of iterations, or a fixed number of times, per user or across all users. It sets no request rate.
- Interleave, Random and Switch each pick one child per pass: in turn, at random, or by a value you compute.
- The Transaction Controller times its children as one sample. In “parent” mode the component reference says the children leave the CSV results file, but with JMeter’s default settings they were still written there.
What are logic controllers in JMeter?
A logic controller is a container in a thread group that changes how the samplers under it run. Some decide whether children run at all (If, Once Only, Throughput), some repeat them (Loop, While, ForEach, Runtime), some pick one child per pass (Interleave, Random, Switch), and some only group or measure (Simple, Transaction). The elements under a controller follow JMeter’s normal scoping rules.
The controllers in JMeter 5.6, grouped by what they do:
| Job | Controllers | What they do |
|---|---|---|
| Run or skip | If, Once Only, Throughput | Run the children only when a condition holds, once per thread, or for a share of iterations |
| Repeat | Loop, While, ForEach, Runtime | Run the children N times, until a condition is false, once per variable value, or for N seconds |
| Pick one child | Interleave, Random, Switch, Random Order | One child per pass in turn, at random, by a switch value; or every child once in random order |
| Group and measure | Simple, Transaction, Recording | Organise the tree, time a group as one sample, or mark where the recorder saves requests |
| Reuse | Module, Include | Run a fragment from the same plan or from another .jmx file |
| Coordinate | Critical Section | Let only one thread at a time run the children |
Where a controller sits in the tree also decides which timers, assertions and pre-processors apply to its samplers. Our guide to the execution order of elements in JMeter covers those scoping rules.
What did one test plan with ten controllers produce?
Every controller in the plan produced the sample count its rule predicts; only the Random Controller’s split changed between runs. We built the plan with a standard thread group of 4 threads × 10 iterations, so every child directly under the thread group ran 40 times in total. A Counter config element numbered each thread’s iterations 1 to 10 as the variable iter. Each controller held one or more HTTP requests to a local test API, and we counted the rows per label in the results file:
| Controller and setting | Sampler | Samples | Why |
|---|---|---|---|
| Once Only | login | 4 | Once per thread |
If, iter is even | if-even | 20 | Iterations 2, 4, 6, 8, 10 of each thread |
| Throughput, 25 percent, shared | tc-25pct | 10 | 25% of the 40 iterations |
| Throughput, 5 total, per user | tc-5-per-user | 20 | 5 for each of 4 threads |
| Throughput, 5 total, shared | tc-5-total | 5 | 5 across all threads |
| Loop, count 3 | loop3 | 120 | 3 per iteration × 40 |
While, page below 3 | while-page | 120 | Pages 0, 1, 2 in every iteration |
| Interleave, children a, b, c | il-a / il-b / il-c | 16 / 12 / 12 | One child per iteration, in turn: a, b, c, a, … |
Switch, iter % 3 | sw-0 / sw-1 / sw-2 | 12 / 16 / 12 | Value 1, 2, 0, 1, … picks child 1, 2, 0, 1, … |
| Random, children x, y | rnd-x / rnd-y | 19 / 21 | One child per iteration at random |
ForEach over item_1 to item_3 | fe-a / fe-b / fe-c | 40 each | One pass per variable, every iteration |
| Transaction (cart + pay) | checkout-tx | 40 | One extra sample per pass, timing both children |
Each count is what the manual predicts, which is the point: once you know the rule, a controller’s output is arithmetic. The Random Controller is the only one that varies between runs; 19 and 21 is one roll of the dice, and an earlier run on JMeter 5.6.2 gave 18 and 22. Our guide to JMeter thread groups covers the settings behind the 4 × 10 shape.
How does the JMeter If Controller work?
The If Controller runs its children only when its condition is true. With “Interpret Condition as Variable Expression” ticked, which is the default and the recommended setting, the condition field must resolve to the text true, ignoring case. Put a variable there, such as ${JMeterThread.last_sample_ok}, which is true when the previous sample passed (useful after a login), or a function that returns true or false, such as ${__jexl3(${count} > 10)} or ${__groovy(...)}.
The condition we used, true on even iterations:
${__groovy(vars.get("iter").toInteger() % 2 == 0)}
Note that it reads the variable with vars.get("iter") instead of writing ${iter} inside the Groovy code. The component reference asks for this: when the script text changes on every call, JMeter cannot cache the compiled script.
Two more settings worth knowing:
- Evaluate for all children. By default the condition is checked once, on entry. Ticked, it is checked before each child, so the controller can stop partway through when a child changes the variable.
- Testing for a missing variable.
${__jexl3("${myVar}" == "\${myVar}")}is true whenmyVaris not set, because JMeter leaves an undefined reference as literal text.${__groovy(vars.get("myVar") == null)}does the same. We ran both withmyVarunset and set: each ran its child only when the variable was missing.
Why should the If Controller condition not use JavaScript?
Because it is far slower. Unticking “Interpret Condition as Variable Expression” makes JMeter evaluate the condition as JavaScript, and the component reference warns that this “has a performance penalty that can be very big and make your test less scalable”. We measured how big, with one thread running the same even-iteration check 20,000 times:
| Condition mode | Condition | Run time for 20,000 iterations (3 runs) |
|---|---|---|
| Variable expression, Groovy | ${__groovy(vars.get("iter").toInteger() % 2 == 0)} | 0.24 s |
| Variable expression, JEXL | ${__jexl3(${iter} % 2 == 0)} | 0.72 s |
| JavaScript | ${iter} % 2 == 0 | 5.4 s |
An iteration with the JavaScript condition took about 270 microseconds against 12 with Groovy, 22 times longer, on Java 11 with its built-in Nashorn engine. Each iteration also ran a Debug Sampler on even passes, the same in all three modes, so the gap is the condition. That time is spent on the load generator’s CPU, between requests. With many threads each evaluating it on every iteration, that CPU time limits how much load one JMeter machine can generate, which is what the manual’s “less scalable” warning is about. The manual still labels the field “Condition (default JavaScript)”, so when you inherit a .jmx file, look for conditions written as bare expressions without a ${...} function around them.
How does the JMeter Throughput Controller work?
The Throughput Controller runs its children for only part of the test, in one of two modes. In percent executions mode it runs them on the given percentage of the iterations that reach it: 25 means one pass in four. In total executions mode it runs them a fixed number of times and then skips them. “Per User” decides whether the count is kept for each thread separately or for all threads together.
Our three Throughput Controllers show the difference, each fed the same 40 iterations:
| Mode | Throughput | Per User | Samples |
|---|---|---|---|
| Percent executions | 25 | No | 10 |
| Total executions | 5 | Yes | 20 (5 × 4 threads) |
| Total executions | 5 | No | 5 |
The manual itself calls the element “badly named, as it does not control throughput”. The element sets how often a branch of the plan runs. For a request rate, use the Constant Throughput Timer or an Open Model Thread Group. The usual job for the percent mode is a traffic mix: put browse, search and checkout branches under three controllers set to 70, 20 and 10, and over many iterations the branches run in that proportion. Each controller decides on its own, so a single iteration can run more than one branch, or none. Where the proportions should come from is covered in our guide to simulating realistic user behavior. Like the Once Only Controller, total mode starts its count again when a parent Loop Controller restarts.
How do the Loop and While Controllers repeat requests?
The Loop Controller runs its children a fixed number of times on every pass, on top of the thread group’s own loop count: our count of 3 under 40 thread iterations produced 120 samples. The While Controller repeats its children until its condition becomes the text false, which suits paging through an API until the last page.
Our While Controller used the condition ${__groovy(vars.get("page").toInteger() < 3)}, with a JSR223 PostProcessor on its request adding 1 to page, and a reset to 0 before the loop. Each iteration fetched pages 0, 1 and 2, then left the loop. Three details from the component reference:
- An empty condition, or
LAST, exits the loop when the last sample inside it fails.LASTalso skips the loop if the sample just before it failed. - The condition is evaluated twice per pass, before and after the children run, so a function with side effects there, such as
__counter, misbehaves. - Both controllers expose their index as
${__jm__<name>__idx}, starting at 0, so a Loop Controller namedLCgives${__jm__LC__idx}.
Put the reset somewhere that runs before each loop. We used a JSR223 PreProcessor under a Flow Control Action sampler set to pause for 0 ms, which records no sample of its own.
What do the Once Only, Interleave, Random and Switch Controllers do?
These four pick what runs on each pass. The Once Only Controller runs its children on the first pass only, per thread, which makes it the usual home of a login request. The Interleave Controller runs one child per pass in turn. The Random Controller runs one child per pass at random. The Switch Controller runs the child whose number or name matches a value you compute.
Our counts show each rule at work. Once Only produced 4 logins, one per thread. Interleave cycled a, b, c, a, b, c, a, b, c, a over each thread’s 10 iterations, so a ran 4 times per thread and b and c 3 times each. The Switch value iter % 3 came out as 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, so child 1 got the extra pass. Two Switch rules from the manual: an out-of-range or empty number runs child 0, and a text value runs the child with that name, or one named default, or nothing.
The Interleave Controller has an “Interleave across threads” option that rotates children across threads as well as iterations: with 4 threads and 3 children, thread 1 starts with the first child, thread 2 with the second, and so on. The Random Order Controller is a different element from the Random Controller: it runs every child once per pass, in a shuffled order.
How does the ForEach Controller loop over values?
The ForEach Controller runs its children once for each variable in a numbered set, item_1, item_2, item_3, putting the current value in an output variable. Our controller read the prefix item and wrote to it, and its HTTP request, labelled fe-${it}, ran 3 times per iteration: 40 samples each for fe-a, fe-b and fe-c.
Numbered variables like these are what a Regular Expression Extractor or JSON Extractor writes when its match number is -1, so the usual pattern is: extract every product ID from a listing page, then visit each one inside a ForEach Controller. When the extractor finds nothing, item_1 is missing and the controller runs no samples, which is a silent gap in the results. Our correlation guide covers setting up those extractors and checking that they matched.
What does the Transaction Controller measure?
It records one extra sample whose time covers every sampler inside it, so a multi-request step such as checkout gets a single response time. By default the time excludes timers and pre- and post-processors; a checkbox includes them. The transaction counts as failed if any child failed.
Our checkout transaction wrapped a cart request (10 ms endpoint) and a pay request (300 ms endpoint). In the HTML report, cart averaged 11.4 ms, pay 301.5 ms and checkout-tx 312.9 ms, the sum of its two children. The report’s Total row counted 699 samples out of 779 rows, leaving out the 80 samples of the plan’s two Transaction Controllers (this one and the parent-mode one below), so they are not counted twice.
“Generate parent sample” changes where the children go. Unticked, the transaction sample is written after its children as an independent sample. Ticked, the children become sub-samples of the transaction. The component reference says that in parent mode the sub-samples “do not appear in CSV log files”. In our run on JMeter 5.6.3, they did: the parent-mode transaction and both its children each got 40 rows in the CSV file, and the HTML report listed all three, exactly like the independent-sample transaction. The setting that removes them is jmeter.save.saveservice.subresults, which defaults to true. In a rerun with -Jjmeter.save.saveservice.subresults=false, the CSV held the parent-mode transaction without its children, while the independent-mode transaction still had its cart and pay rows.
When should you use Module, Include, Runtime and Critical Section Controllers?
These four solve narrower problems. Module and Include reuse steps across a test plan or across files. Runtime puts a time limit on a block. Critical Section stops threads from running a block at the same time. None of them appeared in our counted run.
The Module Controller runs a controller from elsewhere in the same plan, such as a login fragment kept in a disabled thread group, and the Include Controller loads a Test Fragment from another .jmx file. The Include Controller’s file name cannot use variables, and Cookie Managers or User Defined Variables belong in the main plan rather than the included file. The Runtime Controller runs its children over and over until a number of seconds has passed, and 0 means it does not run at all. The Critical Section Controller takes a named lock so only one thread runs its children at a time; the lock covers one JVM only, so in distributed testing each load generator has its own.
Do logic controllers work when JMeter runs in the cloud?
Yes. A logic controller is part of the test plan, so it runs the same way on any machine that runs the plan. The cloud changes the number of threads and leaves the logic as it is: percent-mode Throughput Controllers still split traffic in the same proportions, while total-mode ones with “Per User” unticked count only the threads of their own JMeter instance, because in distributed testing every load generator runs its own full copy of the plan.
LoadFocus runs uploaded JMeter test plans unchanged on cloud machines in 25 regions. You upload the .jmx with its CSV data files and set virtual users, duration and ramp-up for each run in the browser; the product page lists thread groups, timers, assertions and JSR223 samplers among the parts that run as they do on your machine. See JMeter load testing in the cloud. Percent mode is the safer choice for a traffic mix you plan to scale.
Frequently Asked Questions
What is the difference between the Simple Controller and the Transaction Controller?
Both group samplers, but only the Transaction Controller adds anything to the results. The Simple Controller organises the tree and changes nothing about how its children run or report. The Transaction Controller also records one extra sample whose time covers every request inside it, so a step made of several requests gets one response time.
How do I run a request only once per user in JMeter?
Put it in a Once Only Controller directly under the thread group. It runs its children on each thread’s first iteration and skips them afterwards, which is the usual place for a login. Under a Loop Controller it behaves differently: it runs on the first pass of that loop every time the loop starts again.
Is the Throughput Controller the same as the Constant Throughput Timer?
No. The Throughput Controller decides how often a branch of the test plan runs, as a percentage of iterations or a fixed number of executions. The Constant Throughput Timer adds pauses so that a thread group sends a target number of samples per minute. Only the timer affects requests per second.
How do I check if a variable is empty in the JMeter If Controller?
Tick “Interpret Condition as Variable Expression” and use a __groovy condition that is true when vars.get for that variable name returns null or an empty string, which covers both a missing and a blank variable. Without Groovy, a __jexl3 comparison of the variable reference with its own escaped text is true only when the variable was never set, because JMeter leaves an undefined variable as literal text; it does not catch a variable set to an empty value.
Why does my ForEach Controller not run?
Most often because its first input variable does not exist. The controller looks for the prefix followed by _1, such as item_1, and runs nothing when that variable is missing, for example when the extractor that should create it found no match. Check that the extractor’s match number is -1 and that the prefix and the “Use Separator” setting match the variable names.
Sources
- Logic Controllers, section 18.2 of the Apache JMeter User’s Manual, Component Reference: jmeter.apache.org/usermanual/component_reference.html, read 2 November 2026. Quotes on the If Controller’s JavaScript mode, the Throughput Controller’s name and the Transaction Controller’s parent mode are from this page.
- Distributed testing, “each runs the full test plan”: Apache JMeter User’s Manual, Remote Testing, jmeter.apache.org/usermanual/remote-test.html, read 2 November 2026.
jmeter.save.saveservice.subresultsand its default:bin/jmeter.propertiesshipped with JMeter 5.6.3.- LoadFocus JMeter runs: loadfocus.com/jmeter-load-testing, read 2 November 2026.
- Test runs: Apache JMeter 5.6.3 (official release, SHA-512 verified) and 5.6.2 in non-GUI mode on Java 11.0.19, against a local Node.js 23.11.1 test API with 10 ms, 20 ms and 300 ms endpoints, on an Apple M1 Max, 2 November 2026. Timings show patterns, not benchmarks.