The JMeter If Controller Explained
A beginner-friendly but production-minded guide to teaching a load test how to make decisions, so virtual users skip, choose, and react the way real people do instead of marching through the same steps every time.
Think about how you shop online. You open the site, and if you are already signed in, you go straight to your basket. If you are not, you sign in first. If the item you want is sold out, you pick another one. If a pop-up asks about cookies, you click it away. Real people constantly make small decisions like these, and every decision changes which pages they visit and how much work the server must do.
A simple load test does not behave this way. By default, JMeter runs every step in your test plan, in order, for every virtual user, in every round. That is fine for a first experiment, but it is not realistic. Real traffic is a mixture of paths, and a test that ignores this will either overload some parts of the system or never touch others. To give a test the power to decide, JMeter provides the If Controller. It is a small element with a big effect: it looks at a condition, and only runs the steps inside it when the condition is true.
This guide teaches the If Controller from zero. We start with what it is and where it sits in the test plan. Then we look at how its condition is evaluated, the two modes that confuse so many beginners, and a cookbook of useful conditions. After that we move to the topics that separate a beginner script from a production-quality test: error handling, branching without an else, nesting, interaction with timers and transactions, distributed testing, reading results, performance, security, design patterns, and common mistakes. Every new term is explained the first time it appears, and every big idea comes with an everyday comparison.
WWhat Is the If Controller?
The If Controller is a gate. Everything behind it runs only when its condition says yes.
JMeter is a free, open-source tool from the Apache Software Foundation. It pretends to be many users at the same time and measures how a system behaves. A JMeter test is described by a test plan, which is a tree of small elements. Some elements send requests, and these are called samplers. Some elements decide the order and the conditions under which samplers run, and these are called logic controllers. The If Controller is one of the logic controllers.
The simple definition
An If Controller holds a condition, which is a short expression that is either true or false. It also holds a group of child elements, such as samplers and other controllers. When a virtual user reaches the If Controller, JMeter checks the condition. If the condition is true, the children run in their normal order. If it is false, the children are skipped, and the virtual user moves on to whatever comes next in the tree.
Picture a nightclub with a doorman. The doorman checks a rule, for example “guests must be over eighteen”. Guests who satisfy the rule walk through to the dance floor. Guests who do not are simply not admitted, and they continue on their evening elsewhere. The If Controller is the doorman, the condition is the rule, and the children are the dance floor.
What it does not do
Beginners sometimes expect more from the If Controller than it gives. It does not have an “else” branch, so you cannot tell it “do this when true and do that when false”. It does not repeat anything, so it is not a loop. It also does not send a request or create a measurement of its own. It is purely a gatekeeper. If you need an else, a loop, or a random choice, other controllers or simple patterns are used, and we cover them in later chapters.
Because the If Controller makes no request itself, it never appears as a row in your results. You will only see its effect indirectly, through the samplers inside it appearing more or less often than the others.
WWhy Decisions Belong in a Load Test
A test that never decides is a test that never matches real behaviour.
When we say a load test should be realistic, we usually think about the number of users and the amount of think time. But realism also means variety of paths. Real users do not all do the same thing. Some browse and leave. Some search and buy. Some are already signed in, and some are new. Some meet an error and retry. A test that sends every virtual user down the same single path will produce numbers that look tidy but may not match what the server really faces.
Four common reasons to make a test decide
Optional steps
Some actions only happen sometimes, such as accepting a cookie notice or adding gift wrap. The test should run them only when they apply.
Avoiding repeated work
A user who is already signed in should not sign in again. Skipping needless steps keeps the load honest.
Reacting to results
If a step fails or returns an empty list, later steps should not blindly continue and pile up meaningless errors.
Switching features on and off
One test plan can serve several environments or releases, running extra steps only when a setting says so.
What goes wrong without decisions
Imagine a search step that sometimes returns no results. If the next step always tries to open “the first result”, then every empty search triggers an error in the next step. Your error rate climbs, but the cause is not the server. It is your script. In the same way, a test that logs in for every round creates far more login traffic than real life, and then reports that the login system is slow when it is only being abused by an unrealistic script.
A test that decides
- Follows realistic paths and skips needless work
- Avoids cascading errors from earlier failures
- Serves many environments with one plan
- Produces results that are easier to defend
A test that never decides
- Repeats the same path for everyone
- Creates artificial load on some features
- Fills reports with errors caused by the script itself
- Needs many near-copies of the plan for each variation
Decisions add realism, but also add complexity. Every branch is another path you must test, document, and explain. Add a branch only when it reflects something true about the traffic or protects the test from a known failure.
WWhere the If Controller Lives in the Test Plan
Position decides what is gated and how often the gate is checked.
A test plan is a tree. At the top sits the Test Plan, below it one or more thread groups, which are the crowds of virtual users, and below those the controllers and samplers. You add an If Controller by right-clicking a thread group or another controller and choosing to add a logic controller. Then you drag samplers into it, or add them as its children.
flowchart TD
TP["Test Plan"] --> TG["Thread Group: virtual users"]
TG --> S1["Sampler: Open Home Page"]
TG --> IF["If Controller: user is not signed in"]
IF --> S2["Sampler: Login Page"]
IF --> S3["Sampler: Submit Credentials"]
TG --> S4["Sampler: View Basket"]
TG --> IF2["If Controller: basket is not empty"]
IF2 --> S5["Sampler: Checkout"]
How often is the gate checked?
Every time a virtual user’s thread reaches the If Controller while walking down the tree, the condition is evaluated again. If the controller sits directly under a thread group that loops one hundred times, the condition is checked one hundred times for each thread, once per round. If it sits inside a Loop Controller, it is checked on each pass of that loop. This is important because conditions often depend on values that change over time, such as a counter, or a token that was extracted a moment ago.
Who is inside the gate?
Everything placed below an If Controller is governed by it. This includes not only samplers, but also timers, assertions, extractors, and other controllers that are its children. If the condition is false, none of these run. A timer placed inside, for example, adds no think time when the gate is closed, which is usually exactly what you want, but can surprise you if you expected a pause to happen anyway.
| Placement | How often the condition is checked | Typical use |
|---|---|---|
| Directly under a thread group | Once per round of each thread | Skipping optional parts of a journey |
| Inside a Loop Controller | Once per pass of that loop | Running a step on some passes only |
| Inside a Once Only Controller | At most once per thread | Conditional one-time setup |
| Inside another If Controller | Only when the outer condition is true | Combining two conditions step by step |
| Inside a setUp thread group | Once, during preparation | Creating data only if it does not exist |
Name every If Controller after its rule in plain words, such as “If user is not signed in”. When a colleague opens the plan next month, the tree should read like a short story.
AAnatomy of the If Controller Screen
The panel is small. Three items on it decide almost everything.
When you select an If Controller in the tree, the right-hand panel shows only a few fields. Understanding each one removes most of the mystery.
Name and Comments
A label for the tree and a note for humans. Use them generously, because the condition alone rarely explains why the branch exists.
Condition
The expression to evaluate. It can mention variables, properties, and functions. This is the heart of the controller.
Interpret Condition as Variable Expression
Chooses how the condition is read. When ticked, the condition is a variable expression that must produce the word true. When off, it is treated as a script.
Evaluate for all children
When ticked, the condition is rechecked before each child. When off, it is checked once on entry to the controller.
The condition field
The condition field is a single line of text. JMeter replaces any variable references in it with their current values before it decides what to do. So if the field contains a reference to a variable called count, JMeter looks up the value of that variable at the moment the thread reaches the controller. Because the lookup happens each time, the same controller can give different answers in different rounds and for different threads.
Two checkboxes, two big questions
The first checkbox answers the question: how should this text be understood? The second answers the question: how often should it be checked? Each of these gets its own chapter, because getting them wrong is the most common cause of If Controllers that “do nothing” or “run when they should not”. For now, keep a simple rule: use the variable expression mode, and leave the all-children option off unless you have a clear reason.
Older test plans and older tutorials may show the checkbox in a different state or the condition written for the script mode. A condition that works in one mode usually fails silently in the other, so always confirm which mode the plan expects before copying an expression from somewhere else.
Think of a thermostat with two dials. One dial sets the temperature that counts as “too cold”. The other sets how often the thermostat takes a reading. The condition is the first dial; the all-children checkbox is the second. Setting either wrongly gives a house that is cold, or a thermostat that clicks constantly for no reason.
HHow the Condition Is Evaluated: The Internal Working
Follow a single thread as it reaches the gate, from first glance to final decision.
Understanding what JMeter does behind the scenes helps you predict how a condition will behave and why it sometimes does not. The process is short and the same every time.
flowchart TD
A["Thread reaches the If Controller"] --> B["Replace variables and functions in the condition text"]
B --> C{"Variable expression mode?"}
C -- "Yes" --> D["Compare the resulting text with the word true"]
C -- "No" --> E["Run the text as a script and read its result"]
D --> F{"Is it true?"}
E --> F
F -- "Yes" --> G["Run the children in order"]
F -- "No" --> H["Skip the children"]
G --> I["Continue with the next element"]
H --> I
The thread arrives
As the virtual user walks down the tree, it meets the If Controller as the next element in line.
Variables are replaced
Any reference to a variable, such as a token or a counter, is replaced by its present value. Functions in the text are run, and their results are placed into the text.
The result is read
In variable expression mode, the final text is compared with the word true. In script mode, the text is run as a small script and its result is read.
The gate opens or closes
If the answer is true, the thread continues into the children. If not, JMeter skips every child and moves along.
The thread carries on
Either way, the thread proceeds to the next element after the If Controller. Nothing is recorded about the decision itself.
Each thread decides on its own
The condition is evaluated separately for each virtual user. Variables in JMeter belong to a single thread, so one user’s token does not affect another user’s decision. This is why you can have a thousand threads, each with a different answer at the same moment: some are skipping the login while others are running it.
The importance of timing
Because the condition is evaluated at the moment the thread reaches the controller, the order of elements matters. An extractor that stores a value must come before the If Controller that reads it, otherwise the condition sees the old value, or no value at all. In the same way, a counter used in the condition must already have been updated. When a condition seems to use “yesterday’s information”, the cause is almost always an element placed in the wrong order.
The decision itself is very quick in variable expression mode, usually a tiny fraction of a millisecond. The cost grows when the condition needs a script or a heavy function, which is why the choice of mode matters at large scale.
A traffic officer at a junction looks at the current signal, not the signal from five minutes ago. If the signal is updated after the car has already passed, the officer’s decision was based on stale information. Place your updates before the gate.
TThe Variable Expression Checkbox
The single most misunderstood checkbox in the controller. Learn it once and save hours.
The checkbox called “Interpret Condition as Variable Expression” decides how the condition text is understood. It has two settings, and each calls for a different way of writing the same idea.
| Variable expression mode (ticked) | Script mode (not ticked) | |
|---|---|---|
| What the condition must produce | The text “true” for the gate to open | A script whose result is true or false |
| How comparisons are written | Inside a function, such as a JEXL or Groovy function | Directly, like a small program statement |
| Speed | Fast, because no script engine is started for each check | Slower, because a script engine is involved |
| Dependence on Java version | Low | Higher, since the built-in script engine varies between Java versions |
| Recommended today | Yes | Only for old plans that already work |
What “must produce true” means
In variable expression mode, JMeter does all its variable and function replacement, then looks at the final text. If that text is exactly the word true, the gate opens. Anything else, including the word false, an empty string, or an error message, keeps the gate closed. This has a useful consequence: if you have a variable that already holds the word true or false, you can place it alone in the condition field and it will work.
For real comparisons, you use a function that performs the comparison and returns true or false. Two commonly used functions are the JEXL function, which is good for simple comparisons of numbers and text, and the Groovy function, which is more powerful and can read JMeter variables directly. For example, a condition that opens the gate when a counter is above five can be written as ${__jexl3(${count} > 5)}. A condition that opens it when a token variable exists can be written as ${__groovy(vars.get("token") != null)}.
Script mode and why it is fading
In the older script mode, the condition is run by a built-in script engine and written like a statement, for example comparing a quoted variable with a quoted word. This was the original default. It has two problems. First, starting and using a script engine for every check is much slower than variable expression mode, which hurts when thousands of threads check the gate thousands of times. Second, the script engine available inside Java has changed across Java versions, so a plan that worked on one machine may fail on another with a different Java.
If you copy a condition written for script mode into a plan that is in variable expression mode, it will never be true, because the final text is not the word true. The gate stays shut, the steps inside never run, and no error is shown. If a branch never runs, check the mode first.
Recent versions of JMeter normally start new If Controllers with the variable expression box ticked, but plans created long ago may have it unticked. When you inherit a plan, always look at the box before you read the condition.
EEvaluate for All Children
The second checkbox decides whether the gate is checked once, or before every single child.
By default, the If Controller evaluates its condition once, when the thread first reaches it. If the answer is true, all the children run, one after another, without the condition being checked again. This is usually what you want, and it is also the fastest approach.
The checkbox “Evaluate for all children” changes this. When it is ticked, the condition is checked again before each child element. If the condition becomes false in the middle, the remaining children are skipped, even though the first ones ran.
Default: check once
- Cheaper, because only one evaluation per visit
- Easier to reason about: all or nothing
- Right when the answer cannot change while the children run
Check before every child
- Reacts when something changes in the middle of the group
- Costs one extra evaluation for each child
- Can make behaviour harder to predict if the condition depends on moving data
When checking for every child helps
The ticked mode is useful when a step inside the group may change the answer. Imagine a group of five steps that depend on a session that could expire. The condition is “the session is valid”. If the second step fails and marks the session as invalid, you may want steps three to five to be skipped immediately, since they would only produce errors. With the default mode, all five run regardless. With the ticked mode, the gate closes right after the failure.
A museum guard may check your ticket once at the entrance, or at the door of every room. Checking once is quick and fine when tickets are always valid. Checking at every room is slower, but it stops a visitor whose pass expired halfway through the tour.
Do not use it by habit
Ticking the box “just in case” is a poor habit. It adds cost to every child, and it changes the meaning of the group. If your condition is simple and stable, leave the box off. If you notice that you need it, consider whether splitting the group into smaller If Controllers, each with its own condition, would be clearer.
When a condition is evaluated for all children, the check is made before nested controllers as well as samplers. A nested controller counts as a single child, so the condition is not rechecked for the elements inside it unless they have their own gate.
WWriting Conditions: The Basics
A good condition is short, clear, and written so that it can only mean one thing.
Most conditions are built from the same small set of ideas: compare a value with another value, check whether something exists, or combine several checks. Learning a few patterns covers the vast majority of needs.
Comparing numbers
Numbers are compared with the usual signs: greater than, less than, equal, and not equal. A condition such as “the match count is greater than zero” tells you that an extractor found at least one result. Be careful that variables hold text in JMeter. The JEXL function usually converts text that looks like a number into a number for you, but if the variable holds something else, such as an empty string or a literal variable name when the variable does not exist, the comparison may fail or give a surprising answer.
Comparing text
For text, put quote marks around values. The condition “the status equals ok” is written with the word in quotes and compared with a double equals sign. Text comparison is case-sensitive, so “OK” and “ok” are different. If the server may return either, convert both to the same case first, or compare with a function that ignores case.
Checking that something exists
Often the question is not “what is the value” but “is there a value at all”. An extractor that finds nothing leaves its variable unset, or filled with the default you gave it. A reliable habit is to give every extractor a clear default such as NOT_FOUND, and then write the condition as “the variable does not equal NOT_FOUND”. That is far easier to understand than testing for an unset variable.
Combining conditions
You can combine checks using “and” and “or” signs. A condition that must satisfy two things at once uses the double ampersand, and a condition that needs either of two uses the double vertical bar. Parentheses make the order clear. When a combined condition becomes long, consider calculating the answer earlier, in a small helper, and storing it in a single variable named for its meaning, such as needs_login. The condition then reads as plainly as a sentence.
Quote text values
Unquoted words may be read as variable names or cause an error. Quote them every time.
Mind the case
Text comparisons treat uppercase and lowercase as different unless you say otherwise.
Use clear defaults
Give extractors a default so that “not found” is a visible, testable value.
Keep it short
If a condition needs more than one line to explain, move the logic into a helper variable.
If a variable does not exist, JMeter leaves its name in the text instead of replacing it. A condition that compares a missing variable with a word may then compare the literal variable name, and produce a false result without any warning. Always make sure the variable has been set or has a default.
JJEXL and Groovy: Choosing a Function for Your Condition
In variable expression mode, a function does the real work. Two functions cover almost every need.
In variable expression mode, the condition field contains a function call, and the function returns the word true or false. JMeter ships with several functions, but two of them are used far more than the rest. Understanding the difference between them helps you pick the right one each time.
The JEXL function
JEXL is a small expression language. The JEXL function evaluates an expression made of values, comparison signs, and simple logic. It is ideal for questions such as “is this number bigger than that one”, “does this text equal that text”, and “are both of these true”. Its great advantage is simplicity: an expression is short, readable, and cannot do very much, which makes it safe and fast. Use the version called jexl3 in new plans, since the older version is kept only for compatibility.
The Groovy function
Groovy is a full programming language that runs on Java. The Groovy function evaluates a short Groovy statement. It has direct access to JMeter’s variables and properties, through names that JMeter provides, so it can read a variable by name without pasting its text into the expression. This makes it the best choice for checks that JEXL handles awkwardly, such as testing whether a variable exists at all, working with lists, or calling a small helper method. Groovy functions are generally compiled and cached after their first use, so repeated evaluation stays fast.
| Need | JEXL function | Groovy function |
|---|---|---|
| Compare two numbers | Excellent, short and clear | Works, but more than you need |
| Compare two words | Good, with quoted values | Good |
| Check that a variable exists | Awkward | Natural |
| Combine several checks | Good | Good |
| Work with lists or text methods | Limited | Strong |
| Safety against unexpected input | Higher, since it can do little | Needs care, since it can do a lot |
A simple guideline
Start with JEXL for comparisons and combinations. Move to Groovy only when you need to ask about existence, lists, or something JEXL cannot express cleanly. Avoid the older functions that evaluate other script languages inside the condition. They are slower and several of them are kept only for old plans.
JEXL is like a pocket calculator and Groovy is like a laptop. For adding up a bill, the calculator is quicker and cannot go wrong in surprising ways. For anything bigger, you reach for the laptop, accepting that it can do more and therefore needs a more careful operator.
Whichever function you choose, test it on its own first. Paste the whole function into a small sampler’s name or a debug message and run one thread, so you can see exactly what text it produces before you wrap a gate around it.
TText, Numbers, and Other Type Traps
Most “impossible” condition bugs are really small mismatches between what a value looks like and what it is.
JMeter variables are stored as text. When you write a number in a condition, the function may treat it as a number, as text, or as something else, depending on how you wrote it. A handful of traps appear again and again, and knowing them in advance saves hours.
Trap one: numbers that are really text
A variable that holds the characters five and zero looks like the number fifty. If you compare it with another number, a good function will convert it for you. But if the variable also contains a space, a comma, or a currency sign, the conversion fails, and the comparison can silently give the wrong answer. Clean values at the source, by choosing an extractor pattern that captures only the digits.
Trap two: the empty value
An empty variable is different from a missing variable, and both are different from the word NOT_FOUND. A condition written to catch one of these will not catch the others. Decide which of these states your plan can produce, and make the condition cover each of them, or arrange for the plan to produce only one of them by always giving extractors a default.
Trap three: spaces and line breaks
Values extracted from web pages often carry invisible spaces or line breaks at the ends. A value that looks like ok may actually be ok followed by a space, and a comparison with the word ok will fail. Trim the value in the extractor pattern, or compare with a function that ignores surrounding spaces.
Trap four: true and false are just words
In variable expression mode, the gate opens only for the text true. The text True, the number one, the word yes, and the word on do not open it. If a data file or property supplies a switch, make the value exactly true or false, and decide whether the comparison should ignore case.
Trap five: quotes and special characters
A value that contains a quote mark, a bracket, or a comma can break the function around it. This happens when the variable’s text is pasted into the expression. To reduce the risk, read variables through the function’s own lookup rather than pasting them in, and avoid conditions that include free-form text from users or servers.
Capture only what you need
Make the extractor pattern return clean digits or clean words, with no spaces or symbols.
Always set a default
Give every extractor a default, so missing values are never confused with empty ones.
Use exact switch words
Write true and false in lowercase in properties and data files, and compare with the same spelling.
Look at real values
Print the actual value, including its surrounding spaces, in a debug view before writing the condition.
Spaces are invisible in most views. If a comparison fails and everything looks right, check for a hidden space or line break at the end of the value.
TA Condition Cookbook
Common questions a test plan needs to ask, with a condition for each, written for variable expression mode.
Most real conditions come from a short list of recurring situations. The table below gives a recipe for each. Treat the expressions as starting points and adjust names to match your own variables.
| Question | Example condition | Note |
|---|---|---|
| Did the last request succeed? | ${JMeterThread.last_sample_ok} | JMeter sets this variable to true or false after every sampler |
| Is the user not signed in yet? | ${__groovy(vars.get("token") == null)} | Reads the variable at run time, with no text substitution |
| Did the extractor find something? | ${__jexl3("${token}" != "NOT_FOUND")} | Needs a default value on the extractor |
| Did the search return any results? | ${__jexl3(${results_matchNr} > 0)} | Extractors store the number of matches in a variable ending in matchNr |
| Is this the first thread? | ${__jexl3(${__threadNum} == 1)} | Useful for one-time actions per group |
| Is the basket empty? | ${__jexl3(${cartCount} == 0)} | Cart count comes from an extractor reading the basket page |
| Is a feature switched on? | ${__jexl3("${__P(withGift,false)}" == "true")} | Reads a property set when the test starts |
| Does this user have a special role? | ${__jexl3("${role}" == "admin")} | Role comes from a data file column |
| Are both of two things true? | ${__jexl3("${role}" == "admin" && ${cartCount} > 0)} | Join checks with the and sign, and use brackets when mixing and with or |
Reading a condition aloud
A good test of a condition is to read it aloud as a sentence. “If the last sample was ok.” “If the number of results is greater than zero.” “If the role equals admin.” If a condition cannot be said in plain language without stumbling, it is probably too clever, and a future reader will struggle with it. Prefer the clearer form, even if it takes one more line of setup.
A note on the first-round idea
Running something only on the first round of a thread is a popular need. The Once Only Controller is built exactly for this and is clearer than writing a condition for it. Use an If Controller for the first-round idea only when you need to combine it with other conditions.
Keep a short list of your favourite conditions in your team notes, written in the exact form your plans use. A shared cookbook prevents every engineer from inventing a slightly different expression for the same idea.
OOptional Steps and Extractors
The If Controller and extractors are close partners. One finds the facts and the other acts on them.
An extractor is a helper that reads a response and stores a value from it into a variable. Examples include the JSON Extractor, the Regular Expression Extractor, and the CSS Selector Extractor. The If Controller is the natural next step: once a value is stored, the controller decides what to do with it.
The matchNr trick
Most extractors also store the number of matches they found, in a variable named after your variable with a suffix. If your variable is called results, the count is stored in results_matchNr. This count is perfect for an If Controller. A condition that says “the match count is greater than zero” guards any step that needs at least one result. If the search found nothing, the step is skipped instead of failing with an error.
Search request
A sampler sends the search and an extractor stores the identifiers of the results, along with a count.
If Controller checks the count
The condition asks whether the count is greater than zero.
Open the first result
Only when the gate is open, a sampler opens a product using the stored identifier.
Optional follow-up
Further steps, such as adding to the basket, live inside the same gate so they also skip when there was nothing to open.
Defaults make decisions visible
An extractor that finds nothing normally leaves its variable unchanged or unset, depending on its settings. To make the failure explicit, give the extractor a default value, for example NOT_FOUND. Then the condition can ask directly whether the value is something other than NOT_FOUND. A default also keeps the rest of the plan from sending literal variable names to the server when something goes missing.
Choosing an extractor mode that supports your decision
Some extractors can pick the first match, a random match, or all matches. If you plan to loop over all matches with a ForEach Controller, the count is the thing to check first. If you need only one match, ask for the first. An If Controller in front of either one keeps empty results from causing trouble.
Example: a product search that can come back empty
A shop test searches for random words. About one search in ten returns no products. Without a gate, the next request tries to open product number zero and produces an error that the team reads as a server problem. With an If Controller that checks the match count, those searches quietly end, and the error rate shows only real server errors.
A waiter takes your order for the soup of the day. If the kitchen is out of soup, a good waiter does not bring you an empty bowl. They tell you, and you choose something else. The extractor is the waiter’s check with the kitchen; the If Controller is the choice that follows.
EError Handling with the Previous Sample’s Result
One variable lets a test notice failure and react to it.
After each sampler finishes, JMeter stores whether it succeeded in a special variable named JMeterThread.last_sample_ok. It holds the word true when the previous sample passed and false when it failed, including failures caused by assertions. This is a simple way to make an If Controller respond to the immediate past.
What you can do with it
Skip dependent steps
If login fails, skip the steps that need a session, and avoid a long chain of useless errors.
Run cleanup only when needed
If a step that created data failed halfway, run a cleanup step. Otherwise skip it.
Record extra evidence
When a request fails, run an extra sampler or a logging helper to capture details for later study.
Count failures separately
Increase a counter variable on failure so that you can stop a thread after too many errors.
A pattern: guard what follows a risky step
Place an If Controller directly after a risky sampler, with the condition set to the last-sample variable, and put the dependent steps inside it. If the risky step succeeded, the dependent steps run. If not, they are skipped, and the failure is counted once, in the risky sampler, not many times down the chain.
flowchart LR
L["Sampler: Login"] --> G{"If Controller: last sample ok"}
G -- "true" --> A["Sampler: Browse Account"]
A --> B["Sampler: Update Profile"]
G -- "false" --> N["Skip dependent steps"]
B --> NX["Next part of the journey"]
N --> NX
The thread group has an error action too
Do not forget that the thread group has its own setting for what to do after a sampler error: continue, start the next loop, stop the thread, or stop the test. Often that setting is the cleaner way to handle a failed login, because it needs no condition at all. Use an If Controller when you need finer control, such as skipping only part of a journey while still running the rest.
The last-sample variable always refers to the sampler that ran most recently in that thread. If you insert another sampler between the risky one and the If Controller, the variable describes the new sampler instead. Keep the gate directly after the step you mean to check, or save the value into your own variable.
Skipping steps after failures keeps the error report clean, but it also reduces the load placed on the system at the moment it is struggling. In a stress test, be sure that is what you want. A failing system often deserves more traffic, not less, if the goal is to find where it falls over.
DData-Driven Branching with CSV Files
Let the data file decide who does what, so one plan serves many kinds of users.
A CSV Data Set Config element reads a plain text file of rows and hands each virtual user one row. The columns become variables. An If Controller can then branch on the value of any column, which makes the data file a very flexible control panel.
One plan, several kinds of user
Suppose your data file has a column called role, with values such as customer, admin, and guest. A single test plan can contain three If Controllers, one for each role, each holding that role’s journey. A thread that receives a customer row follows the customer path. A thread with an admin row follows the admin path. You can adjust the mix of users in a test simply by editing the data file, with no change to the plan itself.
| username | role | hasBasket |
|---|---|---|
| user001 | customer | true |
| user002 | customer | false |
| user003 | admin | false |
| user004 | guest | false |
Benefits and limits
- Easy mix control. Changing the share of each role means changing the number of matching rows.
- Repeatable. The same file gives the same mix on every run, which makes comparisons fair.
- Visible. People can read the data file and see the planned mix without opening the plan.
- Order matters. If rows are handed out in file order, a file sorted by role will give each role in a block rather than evenly mixed. Shuffle the rows if you want a smooth mixture.
- Sharing mode matters. Whether the file is shared across all threads or per thread changes which thread gets which row. Check the sharing setting to be sure the mix is what you expect.
Passing the data into the condition
The condition simply reads the column variable. For the role example, the condition says that the role equals admin. Because the variable comes from the data file, there is no extra setup. Keep the values in the data file simple and consistent. A role typed once as admin and once as Admin will be treated as two different roles by a case-sensitive comparison.
Count the rows of each role before the test and compare them with the number of requests you later see per branch in the report. If they do not match the planned mix, something in the sharing mode or loop logic is off.
PProperties and Feature Switches
A property turns a branch into a switch you can flip without editing the plan.
JMeter properties are named settings that you supply when the test starts, usually on the command line. They are shared by the whole test, unlike variables, which belong to one thread. Properties make excellent feature switches for an If Controller, because a person starting the test can turn a branch on or off without opening the plan.
What a switch can control
Environment-specific steps
Run a special step only in a certain environment, such as a step that needs a test hook available only in staging.
Heavy or risky actions
Keep expensive steps, such as large uploads, off by default and enable them only for special runs.
New features
Include the journey for a new feature only when the release under test contains it.
Debug helpers
Run a diagnostic sampler only when debugging is switched on, and never in a real load run.
A safe way to write the switch
The property function reads a property and lets you give a default. A condition that tests whether the property called withGift equals true, with a default of false, is written in the same style as the cookbook. Because the default is false, a person who forgets to set the property gets the safer behaviour. Choose defaults that make the plan run in its simplest and least risky form.
| Property | Default | When true |
|---|---|---|
| withGift | false | Some users add gift wrap during checkout |
| debugMode | false | A diagnostic sampler records extra details |
| newCheckout | false | The new payment screen journey is included |
| cleanupAfter | true | A cleanup step removes created records after each round |
Document your switches
A plan with five switches has thirty-two possible combinations. Without documentation, nobody knows which combinations were ever tried. Write the list of switches, their defaults, and their meaning at the top of the plan in a comment, and record the values used with each set of results.
A misspelled property name does not raise an error. JMeter quietly uses the default. If a branch never runs even though you switched it on, compare the spelling of the property in your command with the one in the condition.
AA Worked Example: A Signed-In Shopping Journey
Put the pieces together in one small plan, from first request to final report.
Let us build a plan for an imaginary online shop, step by step, using only ideas from this guide. The goal is a journey in which users sign in only when needed, search for a product, open one if results exist, and optionally add gift wrap.
flowchart TD
A["Sampler: Open Home Page"] --> B{"If Controller A: no session token"}
B -- "open" --> C["Sampler: Login"]
B -- "closed" --> D["Sampler: Search"]
C --> D
D --> E{"If Controller B: results found"}
E -- "open" --> F["Sampler: Open Product"]
F --> G{"If Controller C: gift switch on"}
G -- "open" --> H["Sampler: Add Gift Wrap"]
G -- "closed" --> I["Sampler: Add to Basket"]
H --> I
Open the home page
An ordinary sampler. A cookie manager keeps the session across requests.
Gate A: sign in only if there is no token
A Groovy function checks that the token variable is missing. If so, the login sampler runs, and an extractor stores the new token with a default of NOT_FOUND.
Search for a product
A sampler sends a search with a word taken from a data file. An extractor stores the product identifiers and their count.
Gate B: open a product only if results exist
A JEXL function checks that the match count is greater than zero. Inside the gate, a sampler opens the first product using its stored identifier.
Gate C: gift wrap only when switched on
A property with a default of false controls whether the gift wrap step runs. Inside Gate B, it is only reached when a product was opened.
Add to basket
A final sampler adds the product to the basket. It sits inside Gate B too, because it needs the product.
Checks that make it trustworthy
- Each sampler has a response assertion that checks real content, not only the status code.
- Think time is added by timers placed inside each gate, so skipped steps add no pause.
- Names describe the rule, such as “If no session token” and “If results found”.
- After the run, the counts for login, open product, and gift wrap are compared with the planned shares.
Before scaling this plan, run it with one thread, a data file where one search is known to return nothing, and the gift property switched on and off. Seeing each gate open and close once is worth more than any amount of reading the plan.
TThe Missing Else: Patterns for Two Branches
JMeter has no else. These patterns give you one anyway.
Many programming languages let you write “if this, do that, otherwise do something else”. The JMeter If Controller has no else branch. Experienced testers use a few reliable patterns to get the same effect.
flowchart TD
S["Compute a flag variable: is_member"] --> I1{"If Controller A: flag is true"}
S --> I2{"If Controller B: flag is not true"}
I1 -- "true" --> M["Sampler: Member Journey"]
I2 -- "true" --> G["Sampler: Guest Journey"]
Pattern one: twin controllers with opposite conditions
Create two If Controllers one after the other. The first has a condition and the second has the exact opposite. Exactly one of them opens. This is the closest equivalent to an else and is easy for any reader to understand.
Pattern two: compute the answer first
The twin pattern has a risk. If the steps inside the first controller change something that the second condition reads, the second condition may be evaluated against changed information, and both gates could open, or neither. To avoid this, compute the answer once into a flag variable before either controller, and have both controllers read the flag. The flag does not change while the branches run, so the answers stay consistent.
Pattern three: a Switch Controller
When there are more than two possible paths, such as customer, admin, and guest, a Switch Controller is often a better fit. It reads a value, such as a number or a name, and runs only the child that matches. This gives you “one of many” in a single element rather than several If Controllers, each with an opposite condition that grows more complicated as you add choices.
| Situation | Best tool | Why |
|---|---|---|
| Run or skip one group | If Controller | Simple and direct |
| Either A or B | Two If Controllers with a precomputed flag | Safe and readable |
| One of many named paths | Switch Controller | One element, clean list of choices |
| A fixed percentage of users | Throughput Controller | Built for ratios |
| Random single choice | Random Controller | Built for random selection |
Name the twin controllers so their relationship is obvious, such as “If member” and “Else guest”. A reader should see at once that the two belong together.
NNesting If Controllers
An If Controller can sit inside another. Use this power sparingly.
Because an If Controller can have other controllers as children, you can place one inside another. The inner controller is only reached when the outer condition is true, and then its own condition is checked. The effect is an “and” between the two conditions.
Nesting versus combining
Two conditions can be joined in a single controller with an “and” sign or placed in two nested controllers. Both give the same result in the simple case. The difference is readability and reuse. Nesting is clearer when the outer condition guards a large group and the inner condition guards only a small part of it. A single combined condition is clearer when both rules apply to the same small group of steps.
Good reasons to nest
- The inner rule only makes sense when the outer rule holds
- The outer group contains steps that run regardless of the inner rule
- Splitting avoids writing one very long condition
Reasons to avoid deep nesting
- A tree several levels deep becomes hard to read and review
- Each level multiplies the number of possible paths
- Mistakes in one condition hide steps far below it
Keep the depth small
As a rule of thumb, two levels of nesting are acceptable and three are a warning sign. If you find a third level, step back and ask whether the logic belongs in a flag variable, a Switch Controller, or a separate thread group for that kind of user.
Example: a gift option on checkout
The outer controller asks whether the user reached checkout. The inner controller asks whether this user wants gift wrap. If the outer gate is closed, the inner condition is never even evaluated, which saves a tiny amount of work and avoids reading a variable that might not exist yet.
Think of an office building where you need a badge to enter the lobby, and a second key card to enter one particular floor. You cannot use the floor key until you have passed the lobby. Nested controllers behave like these two doors in a row.
IIf, While, Switch, and Their Cousins
Several controllers make decisions. Picking the right one keeps a plan simple.
JMeter offers a family of logic controllers. Some are close relatives of the If Controller, and mixing them up leads to plans that work but are awkward. A quick comparison clarifies when each is the best fit.
| Controller | What it asks | Typical use |
|---|---|---|
| If Controller | Is the condition true right now? | Run a group once, or skip it |
| While Controller | Is the condition still true? Repeat until it is not | Poll until a job is complete |
| Loop Controller | Have we repeated enough times? | Do something a fixed number of times |
| Switch Controller | Which one of these choices applies? | One of many named paths |
| Throughput Controller | Should this run for a given share of the time? | Realistic mixes of users and actions |
| Random Controller | Pick one child at random | Imitate unpredictable users |
| Once Only Controller | Has this thread done it before? | Run login once per user |
| Runtime Controller | Has the time budget run out? | Run children for a number of seconds |
If versus While
The If Controller checks once and runs its children once. The While Controller checks, runs its children, checks again, and repeats until the condition is false. A common mistake is using an If Controller to wait for something. An If Controller that finds the job not ready will simply skip and move on, rather than waiting. For waiting and polling, use a While Controller with a timer to pause between checks, and protect it with a limit so it cannot spin forever.
If versus Throughput
If you want “twenty percent of users do this”, do not invent a random condition. The Throughput Controller is built for ratios and keeps the percentage correct in a way that random conditions often do not. The If Controller is best at answering questions about state, such as whether a value exists, not questions about proportion.
Use an If Controller to ask “does this apply to me right now?” Use the Throughput or Random Controller to ask “what share of users should do this?” Use a While Controller to ask “are we there yet?”
TTimers, Assertions, and Processors Inside the Gate
Helpers placed inside an If Controller follow the same rules as the samplers they belong to.
A JMeter test plan has several kinds of helper elements. Timers add pauses to imitate thinking. Assertions check whether responses are acceptable. Pre-processors and post-processors prepare requests and read responses. Config elements supply settings. Each of them has a scope, which means the set of samplers it affects, decided by its position in the tree.
What happens when the gate is closed
Elements inside an If Controller only act when the controller’s children run. If the condition is false, the children are skipped, and the helpers inside them are skipped too. This is a very useful feature, because it lets you attach exactly the right helper to exactly the right branch. It also has consequences you should plan for.
| Helper placed inside | Gate open | Gate closed |
|---|---|---|
| Timer | Pause is added before the samplers in its scope | No pause, so the thread moves on without any think time |
| Assertion | Checks the responses of the samplers in its scope | Nothing to check, no verdict is produced |
| Extractor | Reads the response and stores a value | Variable keeps its previous value or stays unset |
| Config element, such as a header manager | Applies to samplers inside the gate | Not used by anything |
| Listener | Records results of the samplers in its scope | Records nothing |
A hidden effect on think time
Consider a journey in which each step is followed by a short pause. If the pause lives inside the gate and the gate closes, the thread skips the step and its pause. The overall journey becomes shorter than one with the gate open, and the thread returns to the top of the loop sooner. When many threads skip a step, the whole crowd cycles faster and sends more requests per second to the other steps. This is not wrong, but it can change the load on the rest of the system, so keep it in mind when you compare runs with different branch mixes.
Stale variables
If an extractor lives inside the gate and the gate closes, the variable it would have set keeps whatever value it held before, which may be from a previous round. A later step that reads the variable may use old information without any warning. A careful plan resets such variables at the start of each round, or gives them a default before the gate, so that “not set this time” is clearly visible.
If an extractor inside a closed gate does not run, a variable from an earlier round may still hold a stale token. The next step then sends a token that has expired, and the server rejects it. The If Controller is innocent; the missing reset is the cause.
Place assertions as close as possible to the sampler they check, inside the same gate. That way, you never need to wonder whether an assertion was skipped because of a branch somewhere above it.
IIf Controllers and Transaction Controllers
Business timings and branches need to be arranged with care so that numbers stay meaningful.
A Transaction Controller groups several samplers and reports their combined time as one business step, such as “Checkout”. It is a favourite of reports, because business readers understand it far better than a list of technical requests. Combining it with an If Controller needs a little thought, because the two controllers answer different questions.
Where to place each one
If Controller outside, Transaction inside
The gate decides whether the whole business step happens. When the gate is closed, no transaction is created at all, so the transaction counts reflect only users who really performed the step. This is usually the cleaner choice.
Transaction outside, If Controller inside
The transaction always starts, but its children may be skipped. Depending on your settings, you may see a transaction with a very small time, which can drag down averages and confuse readers.
Why Option A is usually better
Imagine a step called “Apply voucher” that only twenty percent of users perform. With the gate outside, the report shows a count equal to the number of vouchers actually applied and an honest response time for them. With the transaction outside, the report may show a count for every user, with eighty percent of them showing almost no time. The average looks wonderful, and it means nothing.
flowchart TD
G["Thread Group"] --> I["If Controller: user has a voucher"]
I --> T["Transaction Controller: Apply Voucher"]
T --> S1["Sampler: Enter Code"]
T --> S2["Sampler: Confirm Discount"]
Naming for clarity
Because the If Controller does not produce a row in the report, the names of the transaction and its samplers are the only trace of the branch. Choose names that contain the reason, for example “Apply Voucher” rather than “Step 7”. A reader looking at the final report then understands which branch each number belongs to.
When a transaction is made of branches that run sometimes, always report counts next to averages. A response time without a count tells the reader nothing about how many users experienced it.
MModelling a Realistic Mix: When Not to Use an If Controller
The If Controller answers questions about state. Questions about proportion belong to other tools.
A very common request is “make twenty percent of users do the purchase step”. Many beginners try to build this with an If Controller and a random number, for example a condition that is true when a random number between one and one hundred is less than twenty-one. It works roughly, but it is not the best tool for the job.
Why random conditions are a weak fit
A random condition gives about twenty percent over a long run, but not exactly. In a short test, the real share can drift well away from the target by pure chance. Also, each thread draws its own random number, so the split is not controlled across the whole test. And the intent is hidden inside a clever expression that a reader must decode, instead of being stated plainly.
The better tools
Throughput Controller
Can be set to run its children for a given percentage of the time, or a fixed number of times, and counts across the whole test, so the share stays close to the target.
Random Controller
Picks one of several children at random each time, ideal for mixes such as browse, search, or buy.
Separate thread groups
Give each kind of user their own thread group with their own size, so the mix is fixed by thread counts and easy to see.
Data file columns
Decide the kind of user in the data file and branch with an If Controller on the column value.
If Controller is right for
- Skipping a step when a value is missing
- Reacting to the result of the last request
- Turning a branch on or off with a switch
- Following a role or flag from a data file
If Controller is the wrong tool for
- Exact percentages of users
- Waiting until something is ready
- Choosing among many named paths
- Repeating a step a set number of times
If you want one out of five guests at a party to receive a gift bag, you would count the bags and hand them out in order, not flip a coin for each guest. Counting gives you the share you wanted. Coin flips give you a share that is only right on average.
DDistributed Testing and Thread Numbers
When many machines run the same plan, conditions that use thread numbers or machine details behave in surprising ways.
In distributed testing, one machine called the controller sends the plan to several remote machines called engines. Each engine runs the whole plan on its own. Variables and thread numbers are local to each engine, so conditions that depend on them may not mean what you expect.
“Only thread one” runs once per engine
A condition that says the thread number equals one is true for the first thread of the group on each engine. With five engines, that is five threads, not one. If your goal is “exactly one setup action in the whole test”, this pattern is not enough. Use a setUp thread group, which runs once per engine, together with a design that tolerates repeats, or make the action safe to run more than once.
Properties must reach the engines
A property that you set on the controller may not be known to the engines unless it is sent to them in the way JMeter provides for this. If the engines do not receive a property, they use the default written in your condition. A branch you meant to switch on may then stay off on every engine, even though it appears to work when you test on a single machine.
Data files and branches
If branches depend on columns in a data file, each engine needs its own copy of the file. If the files differ between engines, the mix of branches will differ too. Keep files identical, or deliberately split them, and write down which choice you made.
| Condition based on | Behaviour across engines | Advice |
|---|---|---|
| Thread number | Repeats on every engine | Do not use it for “exactly once” actions |
| Property | Same on all engines only if sent to all | Check that each engine received it |
| Data file column | Depends on each engine’s copy of the file | Keep the files consistent |
| Response from server | Independent per thread | Safe and common |
| Machine name or address | Different for each engine | Use only when you want engines to differ |
Before a large distributed run, test the branching with just two engines and a few threads. If the branch counts from the two engines look right, scaling up will not change the logic.
RReading Results When Branches Exist
A branch changes how many times a request runs, so your report must be read with the branch mix in mind.
In a plain test, every sampler runs the same number of times as the others. With If Controllers, counts differ. A sampler behind a gate runs only as often as the gate opens. This changes how you read throughput, error rates, and averages.
Counts first
Look at the number of samples for each label before looking at response times. If a sampler behind a gate shows a count far from what you planned, either the gate opened too rarely or too often. For example, if you expected twenty percent of users to apply a voucher and the report shows two percent, the condition or the data is off. Fix the plan before drawing any conclusion from the timings.
Error rate depends on the denominator
The error rate of a sampler is the number of its failures divided by the number of times it ran. When a gate closes more often during a struggling period, perhaps because a login failed and later steps were skipped, the denominator shrinks, and the reported error rate may look better than the real experience of users. To avoid this, compare the number of attempted journeys with the number of completed journeys, as well as the per-sampler error rate.
| What to check | Why it matters |
|---|---|
| Samples per label versus plan | Confirms each branch ran about as often as designed |
| Throughput per label over time | Shows whether a branch opened less during stress |
| Error rate with counts | Prevents small denominators from hiding problems |
| Percentiles per label | Branches with few samples need extra caution |
| Journeys started versus finished | Reveals users lost by skipped steps |
Small branches and small samples
A branch that runs for only a few users gives few samples. Percentiles from a handful of values are shaky, so treat them as hints, not facts. If a rare branch is important, run the test long enough, or increase the share of that branch in a dedicated test, so that you collect enough measurements.
Do not merge branches in a report just because they look alike. Averaging the response time of “member checkout” and “guest checkout” into one number hides the fact that one of them may be much slower than the other.
MMonitoring, Logging, and Metrics for Branching Tests
If a gate leaves no trace in the results, you must create one on purpose.
Because the If Controller produces no sample of its own, a silently closed gate leaves no mark. You can finish a test, see healthy numbers, and never learn that an important step was skipped for most users. Good monitoring makes decisions visible.
flowchart LR
G["If Controller decisions"] --> C["Counter variables or labelled samplers"]
C --> R["Results file and Backend Listener"]
R --> D["Dashboard: branch counts over time"]
L["JMeter log"] --> D
SYS["System under test metrics"] --> D
D --> H["Review: did the mix match the plan?"]
Ways to make branches visible
- Use clear labels. Each sampler inside a gate should have a name that includes its branch, so that counts per label reveal how often the branch ran.
- Count skips on purpose. Place a lightweight counter or a log message in a second, opposite gate to count how often the gate stayed closed.
- Compare against the plan. After a run, compare the counts of each branch with the expected ratio, and write the comparison into the report.
- Watch throughput per label over time. A branch whose throughput suddenly drops during the test often points to a condition that started to fail, such as an expired token.
- Use the log while building. JMeter can be asked to log more detail about its controllers for a short test, which helps you see why a gate opened or stayed shut. Turn the extra detail off for real runs, because heavy logging slows the test machine.
Watch the system under test too
Branches change the mix of requests the server receives. A sudden jump in database load might come from a branch opening more often, not from the server weakening. Put server metrics on the same timeline as the branch counts, so a change in one can be matched with a change in the other.
During the first runs of a new plan, use the Debug Sampler and a results viewer on a handful of threads to confirm that each gate opens and closes when you expect. Remove them before the real load run.
PPerformance and Scalability of Conditions
A condition is tiny, but a tiny cost repeated millions of times is no longer tiny.
When your test runs thousands of threads for hours, a condition may be evaluated millions of times. Most conditions are cheap, but a few choices make a real difference, and you should know them before the first large run.
Variable expression mode
Avoids starting a script engine for each check, and is the recommended mode for new plans.
Simple comparisons
A direct comparison of two values costs almost nothing compared with a network request.
Script mode
Starts a script engine for every evaluation, which is noticeably heavier at large scale.
Heavy Groovy logic
Long scripts inside a condition belong in a proper script element with caching, not squeezed into a one-line field.
Evaluate for all children
Multiplies the number of evaluations by the number of children. Use it only when needed.
Condition on remote data
A condition that reads a file or a network resource each time will become a bottleneck.
Keep the heavy work elsewhere
If a decision needs real calculation, do the calculation once, in a script element with compiled-script caching, and store the answer in a variable. The If Controller then just reads that variable. This keeps the controller light, makes the logic testable, and avoids repeating the same work in many places.
Think about the whole chain
The cost of an If Controller is almost always smaller than the cost of what it skips. A well-placed gate that avoids a heavy request saves far more resources than it spends. The real danger is not the condition itself, but tests so large that even tiny overheads add up on the test machine. Always watch the processor and memory of the load generator during your first large run.
Measure, do not guess. If you suspect conditions are slowing the generator, run the same plan with a simplified condition for a few minutes and compare the processor usage of the test machine. The comparison is cheap and answers the question with data.
SSecurity Considerations
A condition is a small piece of code. Treat it with the care you give any code.
Conditions look like harmless one-liners, but in variable expression mode they can call functions, and in script mode they run as scripts. Because JMeter replaces variables with their values before evaluating the text, the content of those variables becomes part of the expression. When a variable holds data that comes from outside, such as a server response, that data can change what the expression does.
The injection risk
Suppose a condition inserts a variable into a script, and the variable holds text taken from a web page. If that text contains script commands, the script engine may run them. This is called injection, and it is one of the oldest security problems in software. In a load-testing tool it is less dramatic than on a public website, but it is still real: your test machines often hold credentials and sit inside private networks.
Safer habits
- Read variables inside the function, not by text substitution. A Groovy function that reads a variable with its own lookup is safer than one in which the variable’s content is pasted into the script text.
- Do not build conditions from untrusted text. Avoid inserting raw server responses or data file contents directly into an expression. Compare them as values instead.
- Prefer simple comparisons. The less a condition can do, the less it can be made to do by surprise.
- Review plans from outside sources. A test plan is a program. Opening one from an unknown source is like running an unknown file.
- Restrict who can edit shared plans. Conditions in plans that run on shared infrastructure should go through review like any other code.
Secrets and switches
If you switch branches by properties, do not put secrets, such as passwords, into properties that are echoed in logs. Use protected files or a secrets service, and have conditions check only harmless flags. Keep switches that unlock risky actions off by default, and require a deliberate choice to turn them on, ideally with a note in the test record about who did it and why.
Be careful with what the branch does
A branch that is rarely taken is easy to forget during a safety review. A branch that sends emails, writes data, or calls a payment service is just as dangerous as a main path, perhaps more so, because nobody is watching it. Make sure every branch uses test data and test endpoints, and that you have approval to run it.
Before the test
Review every condition, confirm no untrusted text is pasted into an expression, and check that rarely used branches use safe data.
During the test
Watch branch counts and error rates, and keep a stop plan ready.
After the test
Remove or disable debugging switches, clean up created data, and store or delete results according to your rules.
AAdvantages, Disadvantages, and Trade-offs
The If Controller is simple and powerful. Like every tool, it brings costs you should know before you rely on it.
Strengths
- Gives a plan the ability to skip, choose, and react
- Very quick to learn: one condition and a group of children
- Works with extractors, data files, properties, and results
- Keeps one plan useful for many environments and releases
- Avoids cascading errors from failed earlier steps
- Cheap to evaluate in the recommended mode
Limitations
- No else branch, so two-way choices need patterns
- Leaves no trace in the results when the gate closes
- Two modes with different syntax confuse beginners
- Conditions are text, so typos fail silently
- Poor fit for exact percentages and waiting loops
- Branches multiply the number of paths to test and explain
Flexibility versus clarity
The more branches a plan has, the more realistic it can be, and the harder it becomes to understand. A plan with no branches is easy to read and easy to reason about, but it may not resemble real traffic. A plan with dozens of nested conditions can imitate real behaviour well, but nobody dares to touch it. Most teams find a sweet spot with a handful of well-named gates that reflect the few differences that really matter.
Control versus risk of silent failure
A skipped step is a silent event. If a bug in a condition keeps a gate closed for everyone, the test still finishes, the error rate may look excellent, and the report may be wrong in a way nobody notices. The price of control is the extra work of checking that each branch ran as intended. This is why the chapters on results and monitoring matter as much as the chapters on writing conditions.
One plan versus many plans
Switches let one plan serve many situations, but they also hide differences between runs. The alternative, keeping separate plans for separate scenarios, is simpler to read but costly to maintain, since every shared change must be made several times. Many teams use a middle path: one plan for the shared core with a small number of documented switches.
If a branch exists for a reason you can state in one sentence about real users, keep it. If it exists “just in case”, remove it. Every gate should earn its place.
DDesign Patterns and Anti-patterns
Patterns are habits that keep branching plans healthy. Anti-patterns are habits that quietly spoil results.
Helpful patterns
Guard clause
Put dependent steps behind a gate that checks the thing they depend on, such as a token or a successful login.
Precomputed flag
Calculate a decision once into a clearly named variable, then let every gate read it. Conditions stay short and consistent.
Default and test
Give extractors a visible default and test for it, so that “not found” is explicit.
Feature switch with safe default
Read properties with the safest default, so forgetting to set one never starts something risky.
Gate outside, transaction inside
Keep business-step counts honest by letting the gate decide whether the transaction exists.
Count the skips
Track how often a gate stays closed, so silent skipping becomes visible in the report.
Anti-patterns to avoid
The pattern
Leaving conditions in script mode in a large plan.
Why it hurts
Every check starts a script engine, which wastes processor time on the load generator and may stop working after a Java upgrade.
Better approach
Switch to variable expression mode and rewrite the conditions with the JEXL or Groovy functions.
The pattern
Using random numbers in a condition to create an exact user mix.
Why it hurts
The real share drifts from the target in short tests, and the intent is hidden inside a clever expression.
Better approach
Use a Throughput Controller, a Random Controller, or separate thread groups for the mix.
The pattern
Using an If Controller to wait for something to be ready.
Why it hurts
The If Controller checks once and moves on, so the thread never waits and later steps fail.
Better approach
Use a While Controller with a timer between checks and a limit on the number of attempts.
The pattern
Twin controllers whose conditions read data that the first branch changes.
Why it hurts
Both gates, or neither, may open, because the second condition sees changed information.
Better approach
Compute a flag first and have both gates read that single flag.
The pattern
Deeply nested gates with long, clever conditions.
Why it hurts
Nobody can review the plan with confidence, and a mistake high in the tree hides many steps below.
Better approach
Limit nesting to two levels, name each gate for its rule, and move logic into flag variables.
The pattern
Never checking that each branch actually ran.
Why it hurts
A closed gate leaves no trace, so a broken condition can hide for weeks while reports look healthy.
Better approach
Compare branch counts with the plan after every run, and add counters for skipped branches.
TTroubleshooting an If Controller That Does Not Behave
When a gate opens at the wrong time, or never, work through the usual suspects in order.
Most If Controller problems come from a small set of causes. Match your symptom to the table and check the likely cause first.
| Symptom | Likely cause | What to check |
|---|---|---|
| The branch never runs | Condition written for the wrong mode, or a variable that is never set | The mode checkbox, and the value of each variable at that moment |
| The branch always runs | Condition is always true, for example comparing a text with itself, or an unquoted word | Quotes, spelling, and the result of the function on its own |
| Comparison of numbers gives odd answers | Variable holds text, or is missing and its name is left in the expression | Print the variable with a Debug Sampler |
| Both twin gates open, or neither | Second condition reads data changed by the first branch | Use a precomputed flag |
| Condition used yesterday’s value | Extractor sits after the gate in the tree | Order of elements |
| Branch does not respond to the property | Property name misspelled, or not sent to remote engines | Spelling, and the engine settings |
| Transaction counts look too high | Transaction outside the gate | Place the gate outside the transaction |
| Plan slower than expected at scale | Script mode, or all-children evaluation, or heavy functions | Mode, checkbox, and processor of the generator |
| Condition works in the graphical window but not later | Different Java versions or missing plugin, or properties not supplied | Java version and start-up properties |
A method for any puzzle
Show the values
Add a Debug Sampler just before the gate and look at the variables the condition uses, exactly as they are at that moment.
Test the condition alone
Copy the condition into a small sampler or a log message so you can see the text it produces, without the gate around it.
Check mode and spelling
Confirm the checkbox state, the quotes, the case, and the names of variables and properties.
Change one thing at a time
Alter a single item and run again with one thread, so you can see its effect clearly.
The cheapest debugging tool is a one-thread, one-round run with a results viewer open. You can see every step that ran, and by omission every step that did not.
DDry-Run Method: Proving Your Conditions Before the Big Test
A short, cheap rehearsal catches nearly every branching bug before it can waste a real test.
The bugs that hurt most in branching plans are silent ones: a gate that never opens, or always opens, while the test still finishes and the report still looks healthy. A rehearsal, run at tiny scale, shows these bugs before they spoil a large run. The method has four stages.
List every gate and its expected behaviour
Write down, for each controller, the condition in plain words and the data that should open it and the data that should close it.
Prepare rows that hit each outcome
Create a tiny data file with one row for each path: one that opens the gate, one that closes it, and one with a missing value to test the default.
Run with one thread and a results viewer
Watch which samplers ran for each row. A Debug Sampler before each gate shows the values the condition used.
Compare with the list
Check that every row followed the path you wrote down. Any mismatch is a bug in the condition, the order of elements, or the data.
A table makes it concrete
| Row | Situation | Gate A: sign in | Gate B: open product |
|---|---|---|---|
| 1 | New user, search finds products | Opens | Opens |
| 2 | Returning user, search finds products | Closed | Opens |
| 3 | New user, search finds nothing | Opens | Closed |
| 4 | Login fails | Opens | Depends on your error handling |
Repeat after every change
The rehearsal is not a one-time task. Whenever you change a condition, reorder elements, or edit the data file, run the four rows again. It takes minutes, and it turns branching from a source of doubt into something you can state with confidence: every path was seen to behave as designed.
Before a play opens, the cast runs a dress rehearsal where every entrance and exit is tried once. Nobody expects the rehearsal to be perfect. They expect it to reveal the door that sticks, while there is still time to fix it.
Keep the rehearsal data file and the expected-path table in the same folder as the plan. Anyone who inherits the plan can then prove it still works in a few minutes.
BBest Practices and Common Mistakes
A short checklist to carry into your next test.
Start with the reason
Write down why the branch exists, in one sentence about real users or about protecting the test.
Use variable expression mode
Choose it for every new plan, and migrate old conditions when you touch them.
Give extractors defaults
Make “not found” a visible value, and test for it directly.
Name gates for their rules
Names like “If user is not signed in” make the tree read as a story.
Keep nesting shallow
Stop at two levels, and move complex logic into flag variables.
Verify every branch
After each run, compare branch counts with the plan before reading any timing.
Document switches and defaults
Write them at the top of the plan and record the values used with each result.
Common mistakes in one list
- Pasting a condition written for script mode into variable expression mode, or the other way around
- Forgetting quotes around text values, or ignoring case differences
- Placing the extractor after the gate that reads its value
- Expecting an else branch that does not exist
- Using an If Controller to wait, loop, or split by exact percentages
- Leaving a debugging sampler or a heavy results viewer switched on during a real run
- Putting a transaction outside a rarely taken branch and averaging empty results
- Never checking whether a branch ran as often as planned
A condition that has never been seen to open or close is only a hope. Run the plan with data that should open the gate and with data that should close it, and watch both outcomes at least once.
RReal-World Scenarios
Five typical situations, described in general terms so you can adapt them.
Sign in only when needed
An online store test begins each round on the home page. Some users arrive with a valid session from the previous round, and others do not. A gate checks whether a session token exists. If not, the login steps run. This stops the test from hammering the login service far more than real customers ever would, and makes the login numbers reflect true demand.
An optional cookie notice
A news site shows a cookie banner to first-time visitors only. The test reads the response of the first page, and a gate checks whether the banner text is present. When it is, an extra request records the choice. Returning visitors skip it. The test follows the real pattern and avoids sending needless requests.
A search that may find nothing
A catalogue test searches for random words. A gate checks the number of matches stored by the extractor. If the count is zero, the user stops. If it is above zero, the user opens a product and may add it to the basket. The error rate then shows only real problems, not empty search results.
One plan, three environments
A team keeps a single plan for development, staging, and a production-like environment. A property named env is read by gates around steps that only make sense in some of them, such as a test hook that exists only in staging. People start the test with the environment name, and the plan configures itself.
Different users from a data file
A booking system has customers, agents, and administrators with very different journeys. The data file has a role column. Three gates, one for each role, hold the matching journeys. Changing the mix of users means editing the file, and the same plan serves a quiet night test and a busy morning test.
One idea, many uses
All five scenarios use the same small element with different conditions. That is the quiet strength of the If Controller: it takes almost no effort to add, and it lets a single test plan behave as many different kinds of users and many different environments.
QQuick Reference
A one-page summary you can keep next to your test plan.
| Topic | Recommendation |
|---|---|
| Mode | Use variable expression mode with the JEXL or Groovy function |
| Evaluate for all children | Leave off unless a child can change the answer |
| Two-way choice | Two gates reading one precomputed flag |
| Many-way choice | Switch Controller |
| Exact percentages | Throughput Controller |
| Waiting | While Controller with a timer and a limit |
| Run once per user | Once Only Controller |
| Error reaction | Last-sample variable, or the thread group error action |
| Missing values | Extractor default plus a test for it |
| Switches | Properties with safe defaults, documented in the plan |
| Transactions | Gate outside, transaction inside |
| Verification | Compare branch counts with the plan after every run |
Five questions before pressing start
- Is each condition written for the mode that is currently selected?
- Does every variable used in a condition have a value or a default at that moment?
- Can I say each gate’s rule aloud in a plain sentence?
- Do I know how often each gate should open, and where I will check it?
- Are all debugging helpers switched off for the real run?
FFrequently Asked Questions
Quick answers to the questions beginners ask most often.
SSummary and Key Takeaways
The If Controller is a small gate with a large effect. It lets a test plan skip steps that do not apply, react to what just happened, and follow different paths for different users, which is how real traffic behaves. Used well, it makes results more realistic and more trustworthy. Used carelessly, it hides problems behind silent skipping. If you remember one idea, let it be that every gate needs a reason you can say aloud, and a way to prove after the run that it opened as often as you planned.
Key Takeaways
- An If Controller runs its children only when its condition is true. It makes no request of its own and leaves no row in the results.
- Know the mode checkbox. Variable expression mode needs a condition that produces the word true, and it is the recommended, faster choice.
- Check once, or check every child. The default evaluates once on entry. Evaluating for all children costs more and should be used on purpose.
- Order matters. The element that sets a value must run before the gate that reads it, and missing values need visible defaults.
- There is no else. Use twin gates reading a precomputed flag, or a Switch Controller for many choices.
- Choose the right controller. Use Throughput or Random controllers for proportions and a While Controller for waiting.
- Verify and protect. Compare branch counts with the plan, keep conditions simple and free of untrusted text, and keep switches safe by default.