Logic Controllers & Transactions
|
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.
This page was written against Apache JMeter 5.6.3 (latest release on 2026-10-14; it runs on Java 8 and later, Java 17 or
later is recommended), with the differences planned for JMeter 6.0 noted where they matter. JMeter 6.0 is not
released: the planned changes come from xdocs/changes.xml on the master branch, need Java 17 or later (Java 21
recommended) and are described as planned, not as shipped behaviour. No plugin is needed on this page: every controller
described here ships with core JMeter. The books on the market target JMeter 2.11 to 3.0; the controllers they describe
still exist, but several defaults and options have changed since (listed at the end of the page). Every field name,
property and rule in this page is taken from the official component reference, properties reference and change logs.
A Thread Group on its own runs its children from top to bottom, once per iteration, for every virtual user. Real users do not behave like that: they log in once and then search many times, they sometimes buy and sometimes only browse, they retry while something is pending, and they follow different paths in different proportions. Logic controllers are the elements that give a test plan that shape. They decide which samplers run, how often and in which order, without sending any request themselves. One of them, the Transaction Controller, also measures: it turns a group of requests into one business-level sample such as "create a loan". This page describes every controller, then uses them to model the Bookshelf workload: 60 % of iterations only search, 30 % browse and borrow a book, 10 % are short, login-heavy sessions. It builds on Test Plan Elements, Execution Order & Scoping and Properties, Variables & Functions, and pairs with Thread Groups: Concurrent Users & Ramp-up and Timers & Think Time.
What a logic controller is
The component reference opens the family with one sentence: logic controllers determine the order in which samplers are processed. A controller is a node of the test plan tree that has children (samplers, other controllers, and the config elements, pre-processors, timers, post-processors, assertions and listeners scoped to them). When a thread reaches the controller, the controller decides which of its children to hand to the thread next, and when it is done.
Three properties follow from that model and explain most of the behaviour on this page:
-
Controllers do not produce samples. Only samplers send requests and produce results. The single exception is the Transaction Controller, which adds a synthetic sample that wraps its children. A Simple Controller named "Search" does not appear in any report.
-
Controllers are scopes. Any element placed directly under a controller applies to the samplers below that controller and to nothing else. Placing a timer, an assertion or a header manager under a Simple Controller is the standard way to limit its scope (see Test Plan Elements, Execution Order & Scoping).
-
Controllers nest, and the Thread Group is itself a controller. The Thread Group’s "Loop Count" is an embedded Loop Controller. Every controller below it runs once per Thread Group iteration unless it loops, skips or selects. A Loop Controller with a count of 2 inside a Thread Group with a loop count of 3 sends 2 × 3 = 6 requests.
All logic controllers are added from the same menu: right-click the Thread Group (or another controller) and choose Add → Logic Controller → <name>. Test Fragment is the exception: it is added under the Test Plan with Add → Test Fragment → Test Fragment.
| Controller | What it decides | JMX test class |
|---|---|---|
Simple Controller |
Nothing: it only groups and scopes its children. |
|
Loop Controller |
Runs its children N times (or forever) per pass. |
|
Once Only Controller |
Runs its children on the first iteration of its parent only. |
|
If Controller |
Runs its children only when a condition is true. |
|
While Controller |
Repeats its children until a condition is false. |
|
ForEach Controller |
Repeats its children once per value of a numbered variable set. |
|
Switch Controller |
Runs exactly one child, chosen by number or name. |
|
Interleave Controller |
Runs one child per pass, in turn. |
|
Random Controller |
Runs one randomly chosen child per pass. |
|
Random Order Controller |
Runs every child once, in random order. |
|
Throughput Controller |
Runs its children on a percentage of iterations, or a fixed number of times. |
|
Runtime Controller |
Repeats its children until a time budget is spent. |
|
Transaction Controller |
Runs its children like a Simple Controller and records one extra sample for the whole group. |
|
Module Controller |
Runs a controller defined elsewhere in the plan (usually under a Test Fragment). |
|
Include Controller |
Runs a Test Fragment loaded from another |
|
Critical Section Controller |
Lets only one thread at a time run its children. |
|
Recording Controller |
Nothing at run time; it is where the recorder stores recorded samplers. |
|
The JMX fragments on this page are shortened to the properties that matter: JMeter writes more properties and the
exact set depends on the version. Build controllers through the GUI and read the XML to review them; do not hand-write
whole plans. Each controller in a .jmx file is followed by a <hashTree> that holds its children.
Simple Controller
The Simple Controller has no behaviour beyond storage: JMeter processes its children exactly as if they were placed directly in the parent. It exists for two reasons: to organise a long plan into readable blocks ("Catalogue", "Loans") and to create a scope for timers, assertions, extractors and managers that should apply to a subset of samplers only.
| Field | Meaning | Required |
|---|---|---|
Name |
Descriptive name shown in the tree. |
No |
The Bookshelf plan groups the two catalogue requests and gives them their own Response Assertion, which therefore does not apply to the loan requests:
<GenericController guiclass="LogicControllerGui" testclass="GenericController" testname="Catalogue"/>
<hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="GET /api/books (search)">
<stringProp name="HTTPSampler.path">/api/books?query=${term}</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
</HTTPSamplerProxy>
<hashTree/>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="GET /api/books/{id}">
<stringProp name="HTTPSampler.path">/api/books/${bookId}</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
</HTTPSamplerProxy>
<hashTree/>
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="200 OK">
<collectionProp name="Asserion.test_strings">
<stringProp name="49586">200</stringProp>
</collectionProp>
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
<intProp name="Assertion.test_type">8</intProp>
</ResponseAssertion>
<hashTree/>
</hashTree>
The property name Asserion.test_strings (sic) is the spelling JMeter really uses in .jmx files.
Loop Controller
The Loop Controller runs its children a fixed number of times each time the thread passes through it, in addition to the Thread Group’s own loop count. It is the natural way to model "a user searches five times per visit" without repeating samplers.
| Field | Meaning | Required |
|---|---|---|
Name |
Descriptive name shown in the tree. Also used in the loop index variable name. |
No |
Loop Count |
How many times the children are iterated each time through the controller. |
Yes, unless Forever is checked |
Forever (checkbox) |
Loop until the thread stops. Inside a Thread Group this means the iteration never ends, so the controllers after it are never reached. |
No |
Loop index. Since JMeter 4.0 the Loop Controller exposes the current iteration as a variable named
__jm__<Name>__idx, starting at 0. A Loop Controller named SearchLoop exposes ${__jm__SearchLoop__idx}. The
same variable exists for the ForEach Controller (4.0) and the While Controller (5.0). Give the controller a name
without spaces so the variable is easy to reference.
Functions in the Loop Count are evaluated more than once. The component reference warns that a function such as
__Random in the Loop Count field may be evaluated several times, once for each child sampler, which gives a
different count per child. Compute a random count once, for example with a User Parameters pre-processor or a JSR223
element that stores it in a variable, and reference the variable.
The embedded Loop Controller of the Thread Group is special. Unless it is set to forever, it stops the thread after the given number of iterations; an ordinary Loop Controller simply returns control to its parent.
Bookshelf example: each visit runs three searches, each with a different term from search-terms.csv, and logs the
index:
<LoopController guiclass="LoopControlPanel" testclass="LoopController" testname="SearchLoop">
<stringProp name="LoopController.loops">3</stringProp>
<boolProp name="LoopController.continue_forever">true</boolProp>
</LoopController>
<hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="GET /api/books (search ${__jm__SearchLoop__idx})">
<stringProp name="HTTPSampler.path">/api/books?query=${term}</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
</HTTPSamplerProxy>
<hashTree/>
</hashTree>
LoopController.continue_forever is true here even though the loop is finite. For a Loop Controller that is not
embedded in a Thread Group, this is the value the GUI writes: it makes the controller restart its count on the next
pass instead of finishing for good. The Forever checkbox is stored as a Loop Count of -1. Putting a function in
the sampler name, as above, makes the three searches appear as separate rows in reports; use it for debugging only,
because many distinct labels make reports harder to read.
With 50 threads, a Thread Group loop count of 10 and this Loop Controller, the plan sends 50 × 10 × 3 = 1,500 search requests.
Once Only Controller
The Once Only Controller runs its children only once per thread and skips them on later iterations. More precisely, it runs during the first iteration of any looping parent: under a Thread Group that is once per virtual user for the whole test; under a Loop Controller set to 5 it is once every 5 passes, because the Loop Controller restarts its iteration count each time it is entered. The component reference suggests it for logins: each thread usually needs to log in only once to establish a session.
| Field | Meaning | Required |
|---|---|---|
Name |
Descriptive name shown in the tree. |
No |
The JMeter change log carries a long-standing known problem: the Once Only Controller behaves correctly under a Thread Group or a Loop Controller, but its behaviour under other parents is "not consistent (or clearly specified)". Keep it as a direct child of the Thread Group or of a Loop Controller.
Bookshelf example, the login of the journey:
<OnceOnlyController guiclass="OnceOnlyControllerGui" testclass="OnceOnlyController" testname="Login once per user"/>
<hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="POST /api/auth/login">
<stringProp name="HTTPSampler.path">/api/auth/login</stringProp>
<stringProp name="HTTPSampler.method">POST</stringProp>
</HTTPSamplerProxy>
<hashTree/>
</hashTree>
The token is extracted from the login response with a JSON Extractor and reused by later requests; the extraction itself belongs to the correlation page later in this guide. A JWT expires: in a long soak test a token obtained once per user can expire mid-test. Either make the server’s token lifetime longer than the test, or re-login on a schedule with an If Controller (next section) driven by the elapsed time.
Interaction with "Same user on each iteration". When the Thread Group’s "Same user on each iteration" option is unchecked and the Cookie Manager’s "Use Thread Group configuration to control cookie clearing" box is checked, the Cookie Manager clears cookies between iterations, so a session kept in cookies is lost after the first iteration while the Once Only Controller does not log in again. For a token sent in a header (as in Bookshelf) the variable survives, so the problem does not arise; for cookie sessions keep the option checked (the default).
If Controller
The If Controller runs its children only when its condition evaluates to true. By default the condition is checked once, when the thread enters the controller; optionally it is checked before every child.
| Field | Meaning | Required |
|---|---|---|
Name |
Descriptive name shown in the tree. |
No |
Condition |
The condition. With the next option ticked, it must be an expression that evaluates to the string |
Yes |
Interpret Condition as Variable Expression? |
When ticked, the condition is only compared with |
Yes |
Evaluate for all children |
When ticked, the condition is re-evaluated before each child; when unticked, only on entry. |
Yes |
Use the expression mode. With the option unticked the If Controller evaluates JavaScript, which the documentation
describes as having a performance penalty that "can be very big". It also depends on the JavaScript engine: Nashorn
was removed from Java 15, and JMeter then falls back to its bundled Rhino engine (the javascript.use_rhino property
forces Rhino). JMeter 4.0 changed the default for newly added If Controllers to the expression mode and shows a warning
when the JavaScript mode is chosen.
Typical expressions from the documentation, adapted to Bookshelf:
| Condition | Meaning |
|---|---|
|
The previous sample succeeded. |
|
The variable |
|
The variable |
|
The variable is defined: an undefined variable stays as the literal text |
Do not put ${...} inside a __groovy condition. The documentation warns that variable references inside the
Groovy script change the script text on every call, so the compiled script cannot be cached. Read variables with
vars.get("name") instead, as in the table above.
Bookshelf example: create a loan only when the viewed book has copies available.
<IfController guiclass="IfControllerPanel" testclass="IfController" testname="If copies available">
<stringProp name="IfController.condition">${__groovy(vars.get("available")?.isInteger() && vars.get("available").toInteger() > 0)}</stringProp>
<boolProp name="IfController.evaluateAll">false</boolProp>
<boolProp name="IfController.useExpression">true</boolProp>
</IfController>
<hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="POST /api/loans">
<stringProp name="HTTPSampler.path">/api/loans</stringProp>
<stringProp name="HTTPSampler.method">POST</stringProp>
</HTTPSamplerProxy>
<hashTree/>
</hashTree>
The && and > are only the XML escaping of && and >; in the GUI the condition is typed as plain
Groovy. A skipped If Controller under a Transaction Controller produces an empty transaction; the HTML report ignores
empty Transaction Controller samples since JMeter 3.1.
While Controller
The While Controller repeats its children until its condition becomes the string false. Like the Loop Controller it
exposes its index as ${__jm__<Name>__idx} (since 5.0), starting at 0.
| Field | Meaning | Required |
|---|---|---|
Name |
Descriptive name shown in the tree. |
No |
Condition |
Blank: exit the loop when the last sample in the loop fails. |
No |
The condition is evaluated twice per pass: once before the children run and once after. Functions with side effects
(__counter, for instance) therefore advance twice per loop and should not be used in it.
Always bound the loop. A While Controller whose condition never becomes false keeps the thread in it forever, and
the generator then hammers one endpoint with no think time if the loop has no timer. Combine the business condition
with a maximum count based on the index.
Bookshelf example: the loan is created, then polled until its status is ACTIVE, at most five times. A JSON Extractor
on POST /api/loans and on GET /api/loans/{id} writes loanStatus; a 500 ms Constant Timer under the controller
spaces the polls.
<WhileController guiclass="WhileControllerGui" testclass="WhileController" testname="PollLoan">
<stringProp name="WhileController.condition">${__groovy(vars.get("loanStatus") != "ACTIVE" && (vars.get("__jm__PollLoan__idx") ?: "0").toInteger() < 5)}</stringProp>
</WhileController>
<hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="GET /api/loans/{id}">
<stringProp name="HTTPSampler.path">/api/loans/${loanId}</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
</HTTPSamplerProxy>
<hashTree/>
<ConstantTimer guiclass="ConstantTimerGui" testclass="ConstantTimer" testname="Poll interval">
<stringProp name="ConstantTimer.delay">500</stringProp>
</ConstantTimer>
<hashTree/>
</hashTree>
ForEach Controller
The ForEach Controller loops over a set of numbered variables such as bookId_1, bookId_2, bookId_3, and on each
pass copies the current value into an output variable. It is designed to work with extractors whose Match No. is
-1: the Regular Expression Extractor, the JSON Extractor and others then store every match as name_1 to name_N.
It exposes its index as ${__jm__<Name>__idx} (since 4.0).
| Field | Meaning | Required |
|---|---|---|
Name |
Descriptive name shown in the tree. |
No |
Input variable prefix |
Prefix of the numbered input variables, for example |
No |
Start index for loop |
Start index, exclusive: the first value used is at start index + 1. Empty means start at 1. |
No |
End index for loop |
End index, inclusive. Empty means continue until the next numbered variable is missing. |
No |
Output variable |
Name of the variable that holds the current value inside the loop. Defaults to an empty name, which is almost never what is wanted. |
No |
Use Separator |
When ticked (the default), the |
Yes |
If prefix_1 is not defined (the extractor found nothing), the ForEach Controller runs nothing.
Bookshelf example: the search response returns a list of books; a JSON Extractor with the path $.items[*].id,
variable name bookId and Match No. -1 produces bookId_1 to bookId_N. The user then opens the first three
results:
<ForeachController guiclass="ForeachControlPanel" testclass="ForeachController" testname="OpenResults">
<stringProp name="ForeachController.inputVal">bookId</stringProp>
<stringProp name="ForeachController.returnVal">currentBookId</stringProp>
<boolProp name="ForeachController.useSeparator">true</boolProp>
<stringProp name="ForeachController.startIndex">0</stringProp>
<stringProp name="ForeachController.endIndex">3</stringProp>
</ForeachController>
<hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="GET /api/books/{id}">
<stringProp name="HTTPSampler.path">/api/books/${currentBookId}</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
</HTTPSamplerProxy>
<hashTree/>
</hashTree>
With a start index of 0 and an end index of 3 the loop uses bookId_1, bookId_2 and bookId_3. If the search
returns 20 results and the end index is left empty, every user opens 20 books per search: the number of requests then
depends on the data, which makes the workload hard to reason about. Bound it.
Switch Controller
The Switch Controller runs exactly one of its children on each pass, the one selected by its switch value. It is the most precise tool for a transaction mix because the choice is explicit and mutually exclusive.
| Field | Meaning | Required |
|---|---|---|
Name |
Descriptive name shown in the tree. |
No |
Switch Value |
The number (from 0) or the name of the child to run. Defaults to 0. |
No |
The selection rules are:
-
An empty value, or a number out of range, runs child 0, which therefore acts as the default for numbers.
-
A non-numeric value selects the child with the same name; the match is case-sensitive.
-
If no name matches, the child named
default(case-insensitive) runs; if there is none, nothing runs. -
Since JMeter 4.0 the value is trimmed, so a stray space before or after a name no longer breaks the match.
The value can be a variable or a function. In the 5.6.3 source code the selection is computed once each time the controller is entered, so a random function in the Switch Value draws once per pass, not once per child. The weighted selector used in the Bookshelf model below relies on this.
Interleave Controller
The Interleave Controller runs one child per pass and moves to the next child on the following pass, wrapping around at the end. Under a Thread Group that is one child per iteration, in turn.
| Field | Meaning | Required |
|---|---|---|
Name |
Descriptive name shown in the tree. |
No |
ignore sub-controller blocks |
When ticked, a child controller is treated like a single request element: only one request of that sub-controller is run per pass, instead of its whole block. |
No |
Interleave across threads |
When ticked (since 3.1), the alternation is shared by all threads instead of being per thread. With 4 threads and 3 children, on the first iteration thread 1 runs child 1, thread 2 child 2, thread 3 child 3 and thread 4 child 1. |
No |
In the JMX file the first checkbox is stored as InterleaveControl.style (0 when ticked, 1, the default, when
not) and the second as InterleaveControl.accrossThreads (sic).
Bookshelf example: alternate the search style per iteration, by title one time and by author the next.
<InterleaveControl guiclass="InterleaveControlGui" testclass="InterleaveControl" testname="Search style">
<intProp name="InterleaveControl.style">1</intProp>
<boolProp name="InterleaveControl.accrossThreads">false</boolProp>
</InterleaveControl>
<hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="GET /api/books (by title)">
<stringProp name="HTTPSampler.path">/api/books?query=${term}</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
</HTTPSamplerProxy>
<hashTree/>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="GET /api/books (by author)">
<stringProp name="HTTPSampler.path">/api/books?query=${author}</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
</HTTPSamplerProxy>
<hashTree/>
</hashTree>
Random Controller
The Random Controller is an Interleave Controller that picks a child at random instead of in turn. Each child has the
same probability: with three children each is chosen about a third of the time. It has the same ignore sub-controller
blocks field (stored as InterleaveControl.style) and no "across threads" option.
| Field | Meaning | Required |
|---|---|---|
Name |
Descriptive name shown in the tree. |
No |
ignore sub-controller blocks |
As for the Interleave Controller. |
No |
The documentation adds a caution that applies to every combination on this page: interactions between controllers, and between the Random Controller and others in particular, can give complex behaviour, so experiment before assuming a result. The Thread Group’s Validate action and a short smoke run with a View Results Tree are the way to check.
Unequal weights need either duplicated children (six references to one journey, three to another, one to the last, typically as Module Controllers so the journeys exist once) or a Switch Controller with a weighted selector, both shown below.
Random Order Controller
The Random Order Controller runs every child exactly once per pass, like a Simple Controller, but in a random order. It models steps a user performs in no fixed order, for example opening a book page and checking a current loan.
| Field | Meaning | Required |
|---|---|---|
Name |
Descriptive name shown in the tree. |
No |
<RandomOrderController guiclass="RandomOrderControllerGui" testclass="RandomOrderController" testname="Any order"/>
<hashTree>
<!-- GET /api/books/{id} and GET /api/loans/{id} samplers here -->
</hashTree>
Throughput Controller
Despite its name, the Throughput Controller does not control throughput (the documentation itself calls it "badly named"; requests per second are set with the timers and thread groups covered in Open Model & Throughput Control). It controls how often its children run, in one of two modes.
| Field | Meaning | Required |
|---|---|---|
Name |
Descriptive name shown in the tree. |
No |
Execution Style |
Percent Executions: run on a percentage of the iterations through the plan. Total Executions: run a fixed number of times, then stop. |
Yes |
Throughput |
In percent mode, a number from 0 to 100 ( |
Yes |
Per User |
Ticked: the decision is computed per thread (in total mode the overall number is threads × Throughput). Unticked: the calculation is global for all threads of the group (in total mode, Throughput is the overall number). |
No |
As with the Once Only Controller, the counters restart when a parent Loop Controller restarts.
| GUI field | JMX property |
|---|---|
Execution Style |
|
Throughput (total mode) |
|
Throughput (percent mode) |
|
Per User |
|
Percent mode is deterministic, not random. In the 5.6.3 source code a Throughput Controller in percent mode runs on an iteration when (100 × executions so far + 50) / iterations so far is below the configured percentage. The controller therefore spreads its executions evenly; it does not toss a coin. That makes long-run proportions exact, but it also means that sibling Throughput Controllers decide independently of each other. Three siblings at 60 %, 30 % and 10 % with Per User unticked give, over the first ten iterations of the group:
| Iteration | Controllers that run |
|---|---|
1 |
60 % |
2 |
30 % |
3 |
60 % |
4 |
none |
5 |
60 % |
6 |
60 %, 30 % and 10 % |
7 |
none |
8 |
60 % |
9 |
30 % |
10 |
60 % |
Over 100 iterations the totals are exactly 60, 30 and 10, but 20 iterations run no journey at all and 10 run all three. The proportions of requests are right; the shape of each session is not. When each iteration must be exactly one journey, put the Throughput Controllers under a Random or Interleave parent, as the documentation hints, or use a Switch Controller with a weighted selector.
Validation. The Thread Group’s Validate action runs one thread for one iteration; the property
testplan_validation.tpc_force_100_pct=true (default false) forces percent-mode Throughput Controllers to 100 % in
that run, so every branch is exercised.
Runtime Controller
The Runtime Controller repeats its children until a time budget is exceeded. It is useful to model "a user browses for about a minute" or to give one phase of a setUp Thread Group a fixed duration.
| Field | Meaning | Required |
|---|---|---|
Name |
Descriptive name shown in the tree. |
Yes |
Runtime (seconds) |
Desired run time in seconds. |
Yes |
The budget is checked between children, so a slow last request can overrun it.
<RunTime guiclass="RunTimeGui" testclass="RunTime" testname="Browse for 60 s">
<stringProp name="RunTime.seconds">60</stringProp>
</RunTime>
<hashTree>
<!-- search and view samplers, each with a think-time timer -->
</hashTree>
With a 2 s think time and 250 ms of response time per request, one pass of a two-request browse block takes about 4.5 s, so a 60 s budget gives about 13 passes, or 26 requests, per user per visit.
Transaction Controller
Individual HTTP samples answer "how fast is GET /api/books/{id}?". Stakeholders ask "how long does it take to
borrow a book?", which is several requests. The Transaction Controller answers that question: it runs its children
like a Simple Controller and generates an additional sample that measures the overall time of the nested samplers.
That sample appears in listeners, JTL files and the HTML report under the controller’s name, next to (or instead of)
the individual samples.
| Field | Meaning | Required |
|---|---|---|
Name |
Shown in the tree and used as the label of the generated sample. It is the transaction name in every report, so
choose a stable business name such as |
Yes |
Generate Parent Sample |
Ticked (parent mode): the generated sample becomes the parent of the nested samples. Unticked (default): it is added as an independent sample after the nested samples. |
Yes |
Include duration of timer and pre-post processors in generated sample |
Ticked: the transaction time includes timers, pre-processors and post-processors inside the controller. Unticked
(the GUI default since 2.11, written explicitly as |
Yes |
| GUI field | JMX property |
|---|---|
Generate Parent Sample |
|
Include duration of timer… |
|
How the transaction sample is computed
-
Time: by default the sum of the nested samplers' times, excluding timers and pre/post-processor time. Depending on the clock resolution it can be slightly longer than that sum, because the clock may tick between the controller’s start and the first sample, and again at the end.
-
Success: the transaction is successful only if all its sub-samples are successful. Since 2.10, in parent mode, a failed transaction sample takes the response code of the first failing child.
-
Bytes and counts: the generated sample aggregates its children.
Worked numbers for the Bookshelf "borrow" transaction: search 150 ms, view book 100 ms, create loan 200 ms and view loan 100 ms, each followed by a Uniform Random Timer of 1,000 ms + up to 4,000 ms (3 s on average, the Z = 3 s used throughout this guide):
| Setting | Reported transaction time |
|---|---|
Include timers unticked (default) |
150 + 100 + 200 + 100 = 550 ms |
Include timers ticked |
550 + 4 × 3,000 ≈ 12,550 ms on average, varying with the random think time |
The default answers "how long does the system make the user wait", which is what a response-time objective is about. Ticking the option answers "how long does the whole journey last, think time included", which is useful for capacity reasoning but must not be compared with a latency objective.
Parent mode versus the default mode
| Aspect | Default (independent sample) | Parent mode |
|---|---|---|
Listeners and reports |
The transaction sample and every child sample appear as separate rows. |
Only the transaction appears in most listeners; the children are visible in View Results Tree as sub-results. |
CSV results files |
Children and transaction are all written. |
Sub-samples are not written to CSV; they can be saved in XML. |
Assertions placed on the controller |
Apply to the samplers below it. |
Apply to the children and to the transaction sample. Wrap the children in a Simple Controller and put the assertions there to limit their scope. |
Nesting |
Nested Transaction Controllers work. |
Parent-mode controllers do not properly support nested Transaction Controllers of either type. |
The default mode is the right choice for a load test whose report should show both the business transactions and the requests behind them; parent mode is useful when only transactions matter and the CSV results file should stay small.
Transactions in the console summary and the HTML report
Two properties decide how transactions are counted outside listeners:
-
summariser.ignore_transaction_controller_sample_result(defaulttrue): since 3.1 the CLI summariser ignores the transaction samples, so thesummary +lines count real requests only. -
jmeter.reportgenerator.exporter.html.show_controllers_only(defaultfalse): whentrue, graphs that support it show only the controller (transaction) samples.
Since 3.1 the HTML report also ignores empty transaction samples, for example a Transaction Controller whose only child sits in an If Controller that did not run.
Bookshelf example of the borrow transaction in the default mode:
<TransactionController guiclass="TransactionControllerGui" testclass="TransactionController" testname="TX Borrow book">
<boolProp name="TransactionController.parent">false</boolProp>
<boolProp name="TransactionController.includeTimers">false</boolProp>
</TransactionController>
<hashTree>
<!-- search, view book, POST /api/loans, GET /api/loans/{id} samplers and their timers -->
</hashTree>
Module Controller, Test Fragment and Include Controller
These three elements are about reuse and are described in detail in Test Plan Elements, Execution Order & Scoping. In short:
-
A Test Fragment (Add → Test Fragment → Test Fragment under the Test Plan) holds a controller and its samplers that run only when something references them.
-
The Module Controller (field Module to Run, stored as the
ModuleController.node_pathcollection) runs a controller chosen from the plan tree at run time. The target is found by its path of names, so every target controller needs a unique, non-default name. -
The Include Controller (field Filename, stored as
IncludeController.includepath) loads a Test Fragment from another.jmxfile. It does not support variables or functions in the file name; theincludecontroller.prefixproperty is prepended to it. Cookie Managers and User Defined Variables belong in the top-level plan, not in the included file.
For the transaction mix, Module Controllers let each journey exist once (in a Test Fragment) while being referenced from a Random Controller several times, which is how unequal weights are obtained without a script.
Critical Section Controller
The Critical Section Controller lets only one thread at a time run its children: it takes a named lock before running them and releases it afterwards. It was added in JMeter 2.12.
| Field | Meaning | Required |
|---|---|---|
Lock Name |
The lock to take. Controllers with the same lock name exclude each other; use different names for unrelated sections. |
Yes |
The lock exists only inside one JVM. In distributed testing each load generator has its own lock, so a test must not rely on all threads of all generators blocking each other.
It serialises load, so it caps throughput. If the protected block takes 100 ms, no more than 10 passes per second can go through it whatever the number of threads; 50 threads queue behind each other. Use it to protect test-side resources, never to model server behaviour.
Bookshelf example: a JSR223 PostProcessor appends every created loan id to a shared file, which the tearDown Thread Group later uses to cancel the loans. Appending from many threads at once can interleave lines, so the append runs in a critical section:
// JSR223 PostProcessor (Groovy) under a Critical Section Controller with Lock Name "loan-ids-file"
def id = vars.get("loanId")
if (id != null) {
new File(vars.get("loanIdsFile")).append(id + System.lineSeparator())
}
<CriticalSectionController guiclass="CriticalSectionControllerGui" testclass="CriticalSectionController" testname="Record loan id">
<stringProp name="CriticalSectionController.lockName">loan-ids-file</stringProp>
</CriticalSectionController>
<hashTree>
<!-- a Flow Control Action sampler (no sample) carrying the JSR223 PostProcessor above -->
</hashTree>
The Flow Control Action sampler is used here only as a carrier: it is the one sampler that does not generate a sample result, so it does not add rows to the report.
Recording Controller
The Recording Controller is a placeholder that marks where the HTTP(S) Test Script Recorder stores the samplers it records. During a test run it behaves exactly like a Simple Controller. The Recording template (File → Templates → Recording) creates one under the Thread Group. After recording, rename it or replace it with Transaction Controllers per page; the recorder can also group requests into new Transaction Controllers itself. The recording workflow is covered on the recording and cURL import page later in this guide.
| Field | Meaning | Required |
|---|---|---|
Name |
Descriptive name shown in the tree. |
No |
Modelling the Bookshelf transaction mix
A workload model states what users do and in which proportions. The model for Bookshelf, derived the way the performance testing fundamentals page describes (access logs and business expectations), is:
| Journey | Steps | Share of iterations |
|---|---|---|
Search only |
search, view one book |
60 % |
Browse and borrow |
search, view book, create loan, view loan |
30 % |
Login-heavy |
log in again, create loan, view loan (short sessions that authenticate every time) |
10 % |
Every virtual user logs in once at the start (Once Only Controller), then repeats: pick one journey, run it as a Transaction Controller, think, repeat.
Three ways to implement the 60/30/10 split are common. They differ in whether each iteration runs exactly one journey.
| Approach | How | Behaviour |
|---|---|---|
Sibling Throughput Controllers |
Three Throughput Controllers in percent mode at 60, 30 and 10, Per User unticked, each wrapping a Transaction Controller. |
Exact long-run request shares, but an iteration can run zero, one or several journeys (see the table in the Throughput Controller section). Simple; fine when only aggregate rates matter. |
Random Controller with duplicated children |
A Random Controller with ten Module Controllers: six pointing at the search fragment, three at the borrow fragment, one at the login-heavy fragment. |
Exactly one journey per iteration, chosen at random with the right weights. No scripting; the weights are visible in the tree but awkward to change. |
Switch Controller with a weighted selector |
A Switch Controller whose value is a |
Exactly one journey per iteration, weights in one expression, easy to parameterise with properties. |
The Bookshelf plan uses the Switch Controller. Its children are the three Transaction Controllers, and the switch value returns their names. The selection is drawn once per pass, and the names match case-sensitively:
<OnceOnlyController guiclass="OnceOnlyControllerGui" testclass="OnceOnlyController" testname="Login once per user"/>
<hashTree>
<TransactionController guiclass="TransactionControllerGui" testclass="TransactionController" testname="TX Login">
<boolProp name="TransactionController.parent">false</boolProp>
<boolProp name="TransactionController.includeTimers">false</boolProp>
</TransactionController>
<hashTree>
<!-- POST /api/auth/login + JSON Extractor for the token -->
</hashTree>
</hashTree>
<SwitchController guiclass="SwitchControllerGui" testclass="SwitchController" testname="Journey mix">
<stringProp name="SwitchController.value">${__groovy(def r = java.util.concurrent.ThreadLocalRandom.current().nextInt(100); r < ${__P(searchPct\,60)} ? 'TX Search only' : (r < ${__P(searchPct\,60)} + ${__P(borrowPct\,30)} ? 'TX Browse and borrow' : 'TX Login heavy'))}</stringProp>
</SwitchController>
<hashTree>
<TransactionController guiclass="TransactionControllerGui" testclass="TransactionController" testname="TX Search only">
<boolProp name="TransactionController.parent">false</boolProp>
<boolProp name="TransactionController.includeTimers">false</boolProp>
</TransactionController>
<hashTree>
<!-- GET /api/books?query=${term}, GET /api/books/${bookId}, each with a Uniform Random Timer -->
</hashTree>
<TransactionController guiclass="TransactionControllerGui" testclass="TransactionController" testname="TX Browse and borrow">
<boolProp name="TransactionController.parent">false</boolProp>
<boolProp name="TransactionController.includeTimers">false</boolProp>
</TransactionController>
<hashTree>
<!-- search, view, If Controller (copies available) with POST /api/loans and the PollLoan While Controller -->
</hashTree>
<TransactionController guiclass="TransactionControllerGui" testclass="TransactionController" testname="TX Login heavy">
<boolProp name="TransactionController.parent">false</boolProp>
<boolProp name="TransactionController.includeTimers">false</boolProp>
</TransactionController>
<hashTree>
<!-- POST /api/auth/login, POST /api/loans, GET /api/loans/${loanId} -->
</hashTree>
</hashTree>
Two details in the selector are deliberate. The percentages come from properties through __P, with the commas inside
the nested function escaped as \, because they sit inside the __groovy argument, so a run can change the mix with
-JsearchPct=70 -JborrowPct=20. And the ${__P(...)} references are resolved before Groovy sees the script; they are
constant for the whole run, so the script text does not change and its compiled form can still be cached. Variables
that change per iteration must be read with vars.get instead, as explained in the If Controller section.
The same selector can be checked from the command line before a long run with a short smoke test and the summariser output:
jmeter -n -t bookshelf-load.jmx -l smoke.jtl -Jthreads=1 -JsearchPct=60 -JborrowPct=30
Creating summariser <summary>
Created the tree successfully using bookshelf-load.jmx
Starting standalone test @ ...
summary + 27 in 00:00:28 = 1.0/s Avg: 131 Min: 78 Max: 262 Err: 0 (0.00%) Active: 1 Started: 1 Finished: 0
...
The numbers above are illustrative. The summary lines count only real requests, because the summariser ignores
transaction samples by default. Count the labels of the transaction samples in the JTL file (for example with the
Aggregate Report or the HTML dashboard) to check that the shares come out near 60/30/10 over a few hundred iterations.
Worked numbers: from the mix to requests per second
Assume the response times above (search 150 ms, view 100 ms, login 250 ms, create loan 200 ms, view loan 100 ms), a think time of 3 s on average after every request, and no extra polls. One iteration of each journey lasts:
| Journey | Duration of one iteration | Requests | Share |
|---|---|---|---|
Search only |
150 + 100 + 2 × 3,000 = 6,250 ms |
2 |
60 % |
Browse and borrow |
150 + 100 + 200 + 100 + 4 × 3,000 = 12,550 ms |
4 |
30 % |
Login-heavy |
250 + 200 + 100 + 3 × 3,000 = 9,550 ms |
3 |
10 % |
The mean iteration lasts 0.6 × 6.25 + 0.3 × 12.55 + 0.1 × 9.55 ≈ 8.47 s. With 50 users at full load the group runs 50 / 8.47 ≈ 5.9 iterations per second:
-
about 3.5 search-only, 1.8 borrow and 0.6 login-heavy transactions per second;
-
0.6 × 2 + 0.3 × 4 + 0.1 × 3 = 2.7 requests per iteration, so about 16 requests per second, in line with the about 15.6 requests per second of the Little’s Law example on the performance testing fundamentals page.
Note what the 60 % applies to: iterations, not requests or time. Search-only journeys are 60 % of iterations but only 1.2 / 2.7 ≈ 44 % of the requests, and the borrow journey, at 30 % of iterations, produces another 44 %. If the workload model was derived from access-log request counts, convert it to iteration shares before configuring the controllers. Because this is a closed model, any slowdown of the server lengthens the iterations and lowers these rates; Open Model & Throughput Control shows how to hold the arrival rate constant instead.
Load-generator cost
Logic controllers run in the virtual user’s thread between samples, so their cost is added to every iteration of every thread. Most of them are negligible; a few are not.
| Controller | Cost on the load generator |
|---|---|
Simple, Loop, Once Only, Interleave, Random, Random Order, Switch, ForEach, Runtime, Recording, Module |
Negligible: a few comparisons per pass. |
If and While in expression mode with |
Small, provided the expression does not embed changing |
If Controller in JavaScript mode |
High: the documentation warns of a very big penalty that makes the test less scalable, and JMeter 5.2 documented a garbage-collection issue in endurance runs with this mode. Avoid it. |
Throughput Controller, Per User unticked |
A shared counter updated under a lock on each iteration: negligible at normal rates. |
Transaction Controller |
One extra sample per transaction: more lines in the results file and more work for listeners and the dashboard. In parent mode the CSV file is smaller because sub-samples are not written. |
Critical Section Controller |
Threads wait for the lock: throughput through the block is capped at 1 / (block duration). |
Include Controller |
Reads the external file when the plan is loaded, not during the run. |
Source: If Controller (Component Reference) and Best Practices
Pitfalls
-
Expecting the Throughput Controller to set requests per second. It sets how often its children run; use the thread groups, timers and open model for rates.
-
Assuming sibling Throughput Controllers are mutually exclusive. They decide independently; some iterations run no journey and some run several. Use a Switch Controller or a Random parent when exactly one journey per iteration matters.
-
Configuring the mix in request shares. The controllers split iterations. Convert first (see the worked numbers).
-
A Switch value that does not match. Names are case-sensitive; with no match and no child called
default, nothing runs and the iteration is silently empty. -
Functions in the Loop Count or While condition with side effects. The Loop Count may be evaluated once per child, and the While condition is evaluated twice per pass.
-
An unbounded While or ForEach loop. Bound every loop by an index or an end index, and put a timer inside polling loops.
-
JavaScript conditions. Keep Interpret Condition as Variable Expression? checked and use
__jexl3or__groovy. -
${...}inside__groovyconditions. It disables script caching; usevars.get. -
Once Only under an unusual parent. Its behaviour is only well defined under a Thread Group or a Loop Controller.
-
Including timers in a transaction and comparing it with a latency objective. Keep Include duration of timer… unticked for response-time objectives.
-
Assertions on a parent-mode Transaction Controller. They also apply to the transaction sample; scope them with a Simple Controller.
-
Nested parent-mode Transaction Controllers. Not properly supported; keep nesting to default-mode controllers.
-
Critical sections in distributed tests. The lock is per JVM; generators do not exclude each other.
-
Duplicate or default controller names with Module Controllers. The target is found by its name path; rename every target controller.
From the books to 5.6 to 6.0
| Topic | Books (JMeter 2.11 to 3.0) | JMeter 5.6.3 and planned 6.0 |
|---|---|---|
If Controller |
JavaScript conditions are common in examples. |
New If Controllers use Interpret Condition as Variable Expression? by default since 4.0, with a warning for the JavaScript mode. Nashorn was removed in Java 15; JMeter uses its bundled Rhino engine. 6.0 (planned) updates Rhino to 1.9.1. |
Loop index |
Counters or |
Loop and ForEach Controllers expose |
Transaction Controller |
2.11 changed the default of Include duration of timer… to unticked for new controllers. |
Same default. Since 3.1 the summariser ignores transaction samples and the HTML report ignores empty transactions. |
Interleave Controller |
Per-thread alternation only. |
Interleave across threads since 3.1. |
Critical Section Controller |
New in 2.12 (Cookbook and PTwJ era). |
Unchanged; per-JVM lock. |
Throughput Controller |
Same two modes. |
Same modes; |
|
BeanShell and BSF are used in the books. |
JSR223 and |
Once Only Controller |
Same behaviour. |
The change log still lists its behaviour outside a Thread Group or Loop Controller as a known problem. |
No other change to the logic controllers is listed in the planned 6.0 change log at the time of writing.
References
-
Apache JMeter source (
xdocs/changes.xmlfor planned 6.0 changes; controller sources undersrc/coreandsrc/componentsfor the Switch selection and Throughput percentage rules) -
Sai Matam and Jagdeep Jain, Pro Apache JMeter, Apress, 2017 (JMeter 3.0), chapter 11: workload modelling with a percentage use-case mix, used for the concept only.
-
Bayo Erinle, Performance Testing with JMeter, 2nd ed., Packt, 2015 (JMeter 2.12), and JMeter Cookbook, Packt, 2014 (JMeter 2.11): background on controllers, used for concepts only.