Thread Groups: Concurrent Users & Ramp-up

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, the latest release on 2026-10-14. It runs on Java 8 or later, and Java 17 or later is recommended. JMeter 6.0 is not released: the changes planned for it on the master branch need Java 17 or later (Java 21 recommended) and are described as planned, not as shipped behaviour. The books that cover thread groups (Bayo Erinle, Performance Testing with JMeter, 2nd ed., chapter 2, on JMeter 2.12; Sai Matam and Jagdeep Jain, Pro Apache JMeter, chapter 5, on JMeter 3.0) describe the same three core fields (threads, ramp-up, loops). Since then the Thread Group has gained Same user on each iteration, lost the absolute start and end times of its scheduler, and its companion elements (validation, running a selection of groups) have appeared. Every field name, property and rule on this page is taken from the component reference, the user manual and, where the manual is silent, the 5.6.3 source code, which is cited as such.

The Thread Group is where load is created. Each of its threads is one virtual user that runs the elements below it from top to bottom, again and again, independently of every other thread. The Thread Group decides how many of those users exist, how quickly they arrive, how long they stay and what they do when a request fails. It implements the closed workload model described in Performance Testing Fundamentals: a fixed population of users, each of which sends its next request only after the previous one has returned. Plans that must hold an arrival rate whatever the response time belong on Open Model & Throughput Control; think time between requests is covered on Timers & Think Time.

The running example is the Bookshelf API (POST /api/auth/login, GET /api/books?query=, GET /api/books/{id}, POST /api/loans, GET /api/loans/{id}) and its test plan bookshelf-load.jmx, with three profiles on this page: smoke (1 user), load (50 users, a 60 s ramp-up, held for 10 minutes) and soak (a steady population for hours).

Where the Thread Group sits

A Thread Group is a direct child of the Test Plan. Every sampler and logic controller must be inside one: the manual describes thread groups as the starting point of every test plan. Elements placed directly under the Test Plan, such as a listener or an HTTP Request Defaults, apply to every Thread Group; elements inside a Thread Group apply only to its threads (the scoping rules are on Test Plan Elements, Execution Order & Scoping).

Test Plan  bookshelf-load.jmx
├── HTTP Request Defaults            (localhost:8080, applies to all groups)
├── setUp Thread Group  "Seed data"   runs first
├── Thread Group        "Borrowers"   the measured load
│   ├── HTTP Cookie Manager
│   ├── CSV Data Set Config (users.csv)
│   ├── Transaction Controller "Journey"
│   │   ├── POST /api/auth/login
│   │   ├── GET  /api/books?query=...
│   │   ├── GET  /api/books/{id}
│   │   ├── POST /api/loans
│   │   └── GET  /api/loans/{id}
│   └── Uniform Random Timer (think time)
├── tearDown Thread Group "Clean up"  runs last
└── Summary Report

Inside the Thread Group, its children are driven by an implicit Loop Controller: that is the element the Loop Count field configures, and one pass through all the children is one iteration. In the .jmx file this controller is stored as the ThreadGroup.main_controller property of the Thread Group.

The life of one thread follows a fixed sequence. JMeter applies the startup delay (if the thread lifetime is specified), then the thread’s own ramp-up offset, then runs iterations until the loop count is used up, the end time is reached, a sampler error action ends the thread, or the test is stopped:

flowchart TD A[Test starts] --> B{Specify Thread lifetime?} B -- Yes --> C[Wait for Startup delay] B -- No --> D[Wait for this thread's ramp-up offset] C --> D D --> E[Run one iteration: the children in tree order] E --> F{Sampler error?} F -- Continue --> G{Loops left and end time not reached?} F -- Start Next Thread Loop --> G F -- Stop Thread --> X[Thread exits] F -- Stop Test or Stop Test Now --> Y[Whole test stops] G -- Yes --> E G -- No --> X

Adding a Thread Group

  1. Start JMeter in GUI mode (bin/jmeter) and open or create bookshelf-load.jmx.

  2. Right-click Test Plan and choose Add → Threads (Users) → Thread Group.

  3. Rename it Borrowers in the Name field.

  4. Fill the fields described in the next section, add the samplers below it, and save with File → Save.

A Thread Group saved by the GUI looks like this in the .jmx file. This is the load profile with literal values; the plan is parameterised with properties later on this page.

<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Borrowers" enabled="true">
  <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
  <elementProp name="ThreadGroup.main_controller" elementType="LoopController"
               guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
    <boolProp name="LoopController.continue_forever">false</boolProp>
    <intProp name="LoopController.loops">-1</intProp>
  </elementProp>
  <stringProp name="ThreadGroup.num_threads">50</stringProp>
  <stringProp name="ThreadGroup.ramp_time">60</stringProp>
  <boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
  <boolProp name="ThreadGroup.delayedStart">false</boolProp>
  <boolProp name="ThreadGroup.scheduler">true</boolProp>
  <stringProp name="ThreadGroup.duration">660</stringProp>
  <stringProp name="ThreadGroup.delay">0</stringProp>
</ThreadGroup>

LoopController.loops set to -1 is how the GUI stores the Infinite check box; the official templates shipped in bin/templates use the same form.

Every field of the Thread Group

The table lists every field of the Thread Group panel in JMeter 5.6.3, the property that stores it in the .jmx file and its behaviour. GUI labels are those of the English resource bundle; descriptions follow the component reference.

GUI field .jmx property Default in a new group Meaning

Name

testname attribute

Thread Group

Descriptive name shown in the tree. It is also the prefix of every thread name (Borrowers 1-1, Borrowers 1-2…​), which appears in the threadName column of the results file and in the log.

Comments

TestPlan.comments

empty

Free text, not used at run time.

Action to be taken after a Sampler error

ThreadGroup.on_sample_error: continue, startnextloop, stopthread, stoptest, stoptestnow

Continue

What happens when a sample fails, either because the sampler itself failed or because an assertion failed. See Action to be taken after a Sampler error.

Number of Threads (users)

ThreadGroup.num_threads

1

Number of users to simulate. Required.

Ramp-up period (seconds)

ThreadGroup.ramp_time

1

How long JMeter takes to get all the threads started. The first thread always starts immediately. Required. See Number of Threads and the ramp-up maths.

Loop Count, with the Infinite check box

LoopController.loops in ThreadGroup.main_controller (-1 for Infinite)

1

Number of times each thread performs the test case. Infinite makes the thread run until the test is stopped or the end of the thread lifetime is reached. Required unless Infinite is selected.

Same user on each iteration

ThreadGroup.same_user_on_next_iteration

selected

If selected, cookie and cache data from the first sampler response are used in later iterations (a Cookie Manager and a Cache Manager are needed). If not selected, they are not reused and a new connection is opened between iterations. See Same user on each iteration. Added in JMeter 5.2.

Delay Thread creation until needed

ThreadGroup.delayedStart

not selected

If selected, each thread is created only when its share of the ramp-up time has elapsed. If not, all threads are created when the test starts and then pause for their share of the ramp-up. See Delay Thread creation until needed.

Specify Thread lifetime

ThreadGroup.scheduler

not selected

Enables the two fields below, which confine the threads' operation time.

Duration (seconds)

ThreadGroup.duration

empty

A relative end time, used to calculate when the threads stop. Only used when Specify Thread lifetime is selected.

Startup delay (seconds)

ThreadGroup.delay

empty

A relative startup delay, used to calculate when the threads start. Only used when Specify Thread lifetime is selected.

Older .jmx files also contain ThreadGroup.start_time and ThreadGroup.end_time (longProp values with epoch milliseconds). They are leftovers of the absolute Start Time and End Time fields that JMeter 4.0 removed from the scheduler (Bug 61549); 5.6.3 still loads such files and the official templates still carry the properties, but the values are not used. The numeric fields are stored as strings (stringProp), which is what lets them contain a function call such as ${__P(threads,1)}.

Action to be taken after a Sampler error

A sampler error is a sample marked as failed: a connection error, a timeout, an HTTP status that the sampler treats as failure, or any failed assertion on that sample. The five choices are:

Choice .jmx value Effect

Continue

continue

Ignore the error and carry on with the next element. The default, and the right choice for most load tests, where errors are counted, not acted on.

Start Next Thread Loop

startnextloop

Ignore the error, abandon the rest of the current iteration and start the next one. Useful when later steps depend on an earlier one: if the Bookshelf login fails there is no token, and sending the four remaining requests would only add four more meaningless errors.

Stop Thread

stopthread

The current thread exits; the other threads continue. The population shrinks with every error, so the load falls.

Stop Test

stoptest

The entire test is stopped at the end of any current samples (a graceful Shutdown).

Stop Test Now

stoptestnow

The entire test is stopped abruptly. Any current samplers are interrupted if possible.

The setting applies to every sampler in the group. When only one request should trigger an action (only the login, for example), leave the group on Continue and put a Result Status Action Handler post-processor under that sampler. It offers the same five choices; its 5.6.3 panel also shows Break Current Loop and Go to the next iteration of Current Loop, which act on the nearest enclosing loop instead of the whole thread loop but are not yet described in the component reference.

When the test runs in CLI mode, the log states the chosen action as each group starts, for example Thread will start next loop on error or Test will stop on error, which is a quick way to check what a plan received from a property.

Stop Test and Stop Test Now make the error rate the end condition of the whole run. They suit a smoke test in a CI job (fail fast if the environment is broken), and are dangerous in a load test, where one timeout among thousands of requests would end the measurement. Stop Thread silently converts a fixed population into a shrinking one, which makes throughput figures from the end of the run meaningless; prefer Start Next Thread Loop.

Number of Threads and the ramp-up maths

Number of Threads is the number of virtual users. Each thread is a Java thread with its own copy of the elements below the Thread Group, its own variables and its own cookies, and runs the elements in its entirety and independently of other threads. "Concurrent users" in JMeter therefore means threads that exist at the same time, not requests in flight: a thread that is sleeping in a timer is still a user.

The Ramp-up period spreads the start of those threads over time. With \(N\) threads and a ramp-up of \(R\) seconds:

\[ \text{delay between thread starts} = \frac{R}{N}, \qquad t_k = (k - 1)\,\frac{R}{N}, \qquad \text{start rate} = \frac{N}{R} \]

where \(t_k\) is the start time of thread \(k\) after the group’s startup delay. The manual’s own example is 10 threads and a ramp-up of 100 s: each thread starts 10 s after the previous one. Because the first thread starts directly, the tenth starts after 90 s, not 100 s, and a group of one thread has an effective ramp-up of zero.

Threads Ramp-up Delay between starts Last thread starts at Start rate

10

100 s

10 s

90 s

0.1 threads per second

30

120 s

4 s

116 s

0.25 threads per second

50

60 s

1.2 s

58.8 s

0.83 threads per second

200

100 s

0.5 s

99.5 s

2 threads per second

50

0 s

0 s

0 s

all at once (a spike)

In the 5.6.3 implementation the delay between starts is computed in milliseconds and rounded (round(rampUp × 1000 / threads)), and each new thread corrects for the time it took to start the previous one, so the schedule does not drift on a busy load generator. While a thread waits for its ramp-up offset it checks for a test stop every jmeterthread.rampup.granularity milliseconds (default 1000), which is why stopping a test during a long ramp-up can take up to a second.

The manual gives two constraints and a starting point. The ramp-up must be long enough not to overload the system at the start of the test, yet short enough for the last threads to start before the first ones have finished. As a first guess, start with a ramp-up equal to the number of threads and adjust. A ramp-up that is too short produces a spike of connection set-ups, logins and cold caches that the system under test would never see in production, and the first minutes of results then describe that spike, not the steady state. A ramp-up that is too long, combined with a finite loop count, means early threads finish before late threads start, so the target concurrency is never reached.

The figure shows the active threads of a group with 50 threads, a 20 s startup delay, a 60 s ramp-up and a 660 s duration, and the alternative ending when the loop count runs out first.

Active threads over time for 50 threads, a 20 s startup delay, a 60 s ramp-up and a 660 s duration, with a zoom on the first starts

Loop Count and Infinite

The Loop Count is the number of iterations each thread performs, not a total for the group: 50 threads with a loop count of 10 run 500 iterations of the journey. With a finite count and no thread lifetime, each thread ends when its own iterations are done. Threads that started later end later, so the end of the test mirrors the ramp-up (the dashed line in the figure), and the steady state lasts only as long as the time between the last thread starting and the first thread finishing.

Infinite (-1 in the file) runs iterations until something else ends the thread: the end of the thread lifetime, a sampler error action, or a Stop or Shutdown of the test. An infinite loop with no thread lifetime runs until it is stopped by hand; in CLI mode that means the shutdown port (4445 by default) or bin/shutdown.sh.

For load tests, a duration is the better end condition: it fixes the measurement window regardless of how fast the server answers, whereas a loop count makes the test shorter when the server is fast and longer when it is slow. A loop count is the better choice for smoke tests and for data-driven runs where each iteration consumes one CSV row and the file holds exactly as many rows as needed.

Specify Thread lifetime: Duration and Startup delay

Selecting Specify Thread lifetime (stored as ThreadGroup.scheduler, the name of the old Scheduler check box) enables Duration (seconds) and Startup delay (seconds). When the test starts, JMeter waits for the startup delay before starting the group’s threads and runs it for the configured duration. When the scheduler is used, the group runs until either the loop count or the duration is reached, whichever occurs first.

Three details matter when choosing the numbers:

  • The duration includes the ramp-up. In 5.6.3 the end time of every thread is calculated as the moment the group starts plus the startup delay plus the duration; the ramp-up offset of each thread is not added to it. A group with a 60 s ramp-up and a 600 s duration holds full load for 540 s at most. To hold for a full 10 minutes, set the duration to ramp-up plus hold: 60 + 600 = 660 s. This behaviour comes from the 5.6.3 source code (ThreadGroup.scheduleThread); the manual only says that the duration is used to work out the end time.

  • The end condition is only checked between samples. When the end time is reached a thread stops, but JMeter does not interrupt samplers that are waiting for a response, so the end may be delayed arbitrarily. Set connect and response timeouts on the HTTP Request (or HTTP Request Defaults) so that a hung server cannot keep a test running long after its window.

  • Timers do not overrun the end. Since JMeter 3.1 (Bug 60049) a timer delay that would end after the scheduled end is shortened, and a thread whose next pause would go past the end time stops at once, so a long think time does not delay the end of the test.

The Startup delay is how to stagger groups that run in parallel: a Browsers group with no delay and an Admins group with a 300 s delay start five minutes apart, even though both are started by the same test. Empty fields count as zero for the delay. An empty or zero duration is not a way to say "no limit": in 5.6.3 the test then stops with an "Invalid duration" error (or, with delayed thread creation, no thread starts at all).

Same user on each iteration

This option decides whether a thread’s next iteration is the same user continuing a visit or a new user arriving.

  • Selected (default): cookie and cache data obtained in one iteration are used in the next, and connections stay open. This needs an HTTP Cookie Manager and an HTTP Cache Manager in the group to have any effect on cookies and cache.

  • Not selected: cookie and cache data are not carried into the next iteration, and a new connection is opened between iterations. The component reference warns that this raises response times (connection and TLS set-up are measured again) and consumes more memory and CPU on the load generator.

The connection behaviour is governed by the property httpclient.reset_state_on_thread_group_iteration (default true): resetting the HTTP state means closing open connections and resetting the SSL state. The Cookie Manager’s own Clear Cookies each Iteration and the Cache Manager’s Clear cache each iteration still exist; in 5.6.3 each of them offers a Use Thread Group configuration to control …​ check box, so the Thread Group can be the single place where the decision is made.

For Bookshelf, where each iteration logs in, borrows a book and leaves, not selected is the more faithful model: every iteration is a new reader with a new connection and a new session cookie. Where the JWT is obtained once in a Once Only Controller and reused for many iterations, keep it selected, because the iterations are one long session. When comparing two runs, keep this option the same, since it changes response times on its own.

Delay Thread creation until needed

By default JMeter creates every thread object when the test starts and makes each one sleep until its ramp-up offset. With Delay Thread creation until needed selected, threads are created only when their share of the ramp-up time has elapsed. The manual recommends it for tests whose ramp-up is significantly longer than the time a thread needs to run its iterations, so that earlier threads finish before later ones start, and best-practices notes that it allows a very large total number of threads as long as only a limited number of them are active at the same time. The option was added in JMeter 2.8.

The option changes memory use, not the load shape: the same threads start at the same times. It matters for plans with thousands of short-lived threads (each runs a few iterations and exits) and makes no difference to a load test whose threads all live until the end, because those threads all exist at full load anyway.

Choosing users, ramp-up and duration

The number of threads follows from the workload, through Little’s Law as explained in Performance Testing Fundamentals:

\[ N = X \times (R_t + Z) \]

where \(N\) is the number of threads, \(X\) the target throughput of iterations (or requests) per second, \(R_t\) the response time and \(Z\) the think time per iteration. The ramp-up follows from how fast real users arrive and how much warm-up the system needs, and the duration from how long the steady state must be observed. The books give the same advice (PTwJ ch. 2 sizes the group from expected concurrent users; Pro ch. 5 stresses a gradual ramp-up and a steady period long enough to measure); the formulas above make it quantitative.

The Bookshelf journey has five requests. Measured at light load, each takes 0.2 s on average and is followed by 3 s of think time, so one iteration takes 5 × (0.2 + 3) = 16 s.

Profile Threads Ramp-up Loop Count Thread lifetime Startup delay

Smoke

1

1 s

1

not specified

none

Load

50

60 s

Infinite

Duration 660 s

0 s

Soak

30

300 s

Infinite

Duration 14700 s

0 s

Smoke. One thread, one iteration: five requests in about 16 s. It checks that the script, data and environment work before any load is applied. The ramp-up is irrelevant (one thread starts immediately).

Load. 50 threads with a 60 s ramp-up start one thread every 1.2 s (0.83 per second). At full load each thread sends 5 / 16 = 0.3125 requests per second, so the group sends 50 × 0.3125 ≈ 15.6 requests per second, or 3.1 journeys per second. The 660 s duration gives the 60 s ramp-up plus a 600 s hold, during which the group sends about 15.6 × 600 ≈ 9,400 requests. Analyse only the hold: exclude the first 60 s, when fewer than 50 users were active.

Soak. The goal is to keep a moderate, steady population for hours to expose memory leaks, connection pool exhaustion and growing tables. 30 threads, a gentle 300 s ramp-up (one thread every 10 s) and a 4 h hold give a duration of 300 + 14,400 = 14,700 s. The group sends 30 × 0.3125 ≈ 9.4 requests per second, about 135,000 requests over the hold, and each of the 30 threads creates a loan in every iteration, so the setUp group must seed enough books and the tearDown group must remove about 27,000 loans. A soak at a constant arrival rate rather than a constant population is built with the Open Model Thread Group on Open Model & Throughput Control.

Two consequences of the closed model are worth stating for every profile. If the server slows down, each thread sends fewer requests per second, so the load falls exactly when the system struggles; check the measured throughput against the target after every run. And a Thread Group never sends more than one request at a time per thread, so 50 threads can never produce more than 50 concurrent requests, however short the think time.

Parameterising users, ramp-up and duration for CLI runs

Load profiles should differ only in their numbers, not in their .jmx file. Type __P function calls in the Thread Group fields instead of literal values; each reads a JMeter property and falls back to its default. The properties and their precedence are covered on Properties, Variables & Functions.

Field Value typed in the GUI

Action to be taken after a Sampler error

Start Next Thread Loop

Number of Threads (users)

${__P(threads,1)}

Ramp-up period (seconds)

${__P(rampup,1)}

Loop Count

${__P(loops,1)} (-1 means Infinite)

Specify Thread lifetime

selected

Duration (seconds)

${__P(duration,60)}

Startup delay (seconds)

${__P(delay,0)}

The resulting fragment, with a Loop Count that is itself a property (-1 gives an infinite loop):

<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Borrowers" enabled="true">
  <stringProp name="ThreadGroup.on_sample_error">startnextloop</stringProp>
  <elementProp name="ThreadGroup.main_controller" elementType="LoopController"
               guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
    <boolProp name="LoopController.continue_forever">false</boolProp>
    <stringProp name="LoopController.loops">${__P(loops,1)}</stringProp>
  </elementProp>
  <stringProp name="ThreadGroup.num_threads">${__P(threads,1)}</stringProp>
  <stringProp name="ThreadGroup.ramp_time">${__P(rampup,1)}</stringProp>
  <boolProp name="ThreadGroup.same_user_on_next_iteration">false</boolProp>
  <boolProp name="ThreadGroup.delayedStart">false</boolProp>
  <boolProp name="ThreadGroup.scheduler">true</boolProp>
  <stringProp name="ThreadGroup.duration">${__P(duration,60)}</stringProp>
  <stringProp name="ThreadGroup.delay">${__P(delay,0)}</stringProp>
</ThreadGroup>

With the scheduler selected, the smoke profile ends after its single loop or after 60 s, whichever comes first. Each profile is then one command line. The smoke run uses the defaults:

jmeter -n -t bookshelf-load.jmx -l smoke.jtl

The load run overrides four properties:

jmeter -n -t bookshelf-load.jmx -l load.jtl -e -o report-load \
  -Jthreads=50 -Jrampup=60 -Jloops=-1 -Jduration=660

and the soak run reads its values from a file, soak.properties:

threads=30
rampup=300
loops=-1
duration=14700
jmeter -n -t bookshelf-load.jmx -q soak.properties -l soak.jtl

In CLI mode the summariser prints a line every 30 s by default. The + lines cover the last interval and end with the thread counts, which make the ramp-up visible; the = lines are running totals. Representative output of the load run (numbers depend on the machine and the target):

Creating summariser <summary>
Created the tree successfully using bookshelf-load.jmx
Starting standalone test @ 2026 Oct 14 10:00:00 CEST (1791964800000)
Waiting for possible Shutdown/StopTestNow/HeapDump/ThreadDump message on port 4445
summary +     47 in 00:00:28 =    1.7/s Avg:   198 Min:    41 Max:   612 Err:     0 (0.00%) Active: 24 Started: 24 Finished: 0
summary +    326 in 00:00:30 =   10.9/s Avg:   205 Min:    38 Max:   701 Err:     0 (0.00%) Active: 49 Started: 49 Finished: 0
summary =    373 in 00:00:58 =    6.4/s Avg:   204 Min:    38 Max:   701 Err:     0 (0.00%)
summary +    468 in 00:00:30 =   15.6/s Avg:   201 Min:    40 Max:   689 Err:     0 (0.00%) Active: 50 Started: 50 Finished: 0
summary =    841 in 00:01:28 =    9.6/s Avg:   202 Min:    38 Max:   701 Err:     0 (0.00%)
...
summary +    470 in 00:00:30 =   15.7/s Avg:   199 Min:    39 Max:   654 Err:     0 (0.00%) Active: 0 Started: 50 Finished: 50
summary =   9764 in 00:11:00 =   14.8/s Avg:   201 Min:    37 Max:   912 Err:     0 (0.00%)
Tidying up ...    @ 2026 Oct 14 10:11:01 CEST (1791965461000)
... end of run

The Active count climbs by about 25 every 30 s (0.83 threads per second) and stays at 50 during the hold; the rate settles at the 15.6 requests per second predicted above (the final running total averages less, about 14.8 per second, because it includes the ramp-up). A rate that keeps falling while Active stays at 50 means response times are growing: the closed model is throttling itself.

In a distributed run, define these properties with -G instead of -J, so that every load generator receives them. Each generator runs the whole plan, so -Gthreads=50 on four generators means 200 users in total.

Several Thread Groups in one plan

A plan can hold several Thread Groups. By default they all start at the same time and run in parallel, which is how a workload with different kinds of users is modelled: for Bookshelf, a Browsers group of 40 threads that only searches and views books, and a Borrowers group of 10 threads that logs in and creates loans. Each group has its own threads, ramp-up and duration; giving each its own startup delay staggers them.

Selecting Run Thread Groups consecutively (i.e. one at a time) on the Test Plan (TestPlan.serialize_threadgroups) runs the groups in tree order, each finishing before the next starts. That turns groups into phases (warm-up, then measured load), at the cost of a total run time that is the sum of all groups. The option and its trade-offs are covered on Test Plan Elements, Execution Order & Scoping.

Parallel (default) Consecutive

Start of each group

All at test start (plus each group’s startup delay)

When the previous group has finished

Total load

Sum of all groups' threads at the same time

One group’s threads at a time

Total run time

The longest group

The sum of all groups

Typical use

A realistic mix of user types

Phases: warm-up, then load; or one measurement per group

Any group can also be run on its own from the GUI: select one or more Thread Groups, right-click, and choose Start, Start no pauses (timers ignored) or Validate. Validation runs the selection with one thread, one iteration, no timers and no startup delay, and can be tuned in user.properties with testplan_validation.nb_threads_per_thread_group, testplan_validation.ignore_timers, testplan_validation.number_iterations and testplan_validation.tpc_force_100_pct. Running a selection and Validate were both added in JMeter 3.0.

setUp and tearDown Thread Groups

A setUp Thread Group (Add → Threads (Users) → setUp Thread Group) is a Thread Group with exactly the same fields that runs before the regular Thread Groups; a tearDown Thread Group runs after them. Use them for preparation and clean-up that must not be measured as part of the load:

  • setUp: create the test books through POST /api/books, create test users, warm a cache, or read a value (an admin token, an ID range) and publish it as a property with __setProperty so the main groups can read it with __P. Properties, not variables, are the way to pass data between groups, because variables belong to one thread.

  • tearDown: delete the loans and books created by the test, or reset the database through an admin endpoint.

The engine starts all setUp groups, waits until every setUp thread has ended, then starts the regular groups, waits for all of them to end, and then starts the tearDown groups. With Run Thread Groups consecutively selected, the groups inside each of the three phases also run one at a time.

When the tearDown groups run is the subtle part. By default a tearDown group does not run if the test is gracefully shut down (Shutdown in the GUI, the shutdown port or bin/shutdown.sh in CLI mode). Selecting Run tearDown Thread Groups after shutdown of main threads on the Test Plan (TestPlan.tearDown_on_shutdown) makes it run in that case. If the test is stopped (Stop, Stop Test Now, bin/stoptest.sh), tearDown does not run even with the option selected. A test that ends normally always runs its tearDown groups.

A complete minimal plan with the three kinds of group follows. The samplers are left out (each hashTree would hold the HTTP Requests); everything else is as saved by JMeter 5.6.3.

<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.6.3">
  <hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="bookshelf-load">
      <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
      <boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
      <boolProp name="TestPlan.functional_mode">false</boolProp>
      <elementProp name="TestPlan.user_defined_variables" elementType="Arguments"
                   guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables">
        <collectionProp name="Arguments.arguments"/>
      </elementProp>
    </TestPlan>
    <hashTree>
      <SetupThreadGroup guiclass="SetupThreadGroupGui" testclass="SetupThreadGroup" testname="Seed data">
        <stringProp name="ThreadGroup.on_sample_error">stoptest</stringProp>
        <elementProp name="ThreadGroup.main_controller" elementType="LoopController"
                     guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller">
          <boolProp name="LoopController.continue_forever">false</boolProp>
          <stringProp name="LoopController.loops">1</stringProp>
        </elementProp>
        <stringProp name="ThreadGroup.num_threads">1</stringProp>
        <stringProp name="ThreadGroup.ramp_time">1</stringProp>
      </SetupThreadGroup>
      <hashTree/>
      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Borrowers">
        <stringProp name="ThreadGroup.on_sample_error">startnextloop</stringProp>
        <elementProp name="ThreadGroup.main_controller" elementType="LoopController"
                     guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller">
          <boolProp name="LoopController.continue_forever">false</boolProp>
          <stringProp name="LoopController.loops">${__P(loops,1)}</stringProp>
        </elementProp>
        <stringProp name="ThreadGroup.num_threads">${__P(threads,1)}</stringProp>
        <stringProp name="ThreadGroup.ramp_time">${__P(rampup,1)}</stringProp>
        <boolProp name="ThreadGroup.same_user_on_next_iteration">false</boolProp>
        <boolProp name="ThreadGroup.scheduler">true</boolProp>
        <stringProp name="ThreadGroup.duration">${__P(duration,60)}</stringProp>
        <stringProp name="ThreadGroup.delay">0</stringProp>
      </ThreadGroup>
      <hashTree/>
      <PostThreadGroup guiclass="PostThreadGroupGui" testclass="PostThreadGroup" testname="Clean up">
        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
        <elementProp name="ThreadGroup.main_controller" elementType="LoopController"
                     guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller">
          <boolProp name="LoopController.continue_forever">false</boolProp>
          <stringProp name="LoopController.loops">1</stringProp>
        </elementProp>
        <stringProp name="ThreadGroup.num_threads">1</stringProp>
        <stringProp name="ThreadGroup.ramp_time">1</stringProp>
      </PostThreadGroup>
      <hashTree/>
    </hashTree>
  </hashTree>
</jmeterTestPlan>

The setUp group uses Stop Test: if the seed data cannot be created there is no point in starting the load. The element names SetupThreadGroup and PostThreadGroup are the aliases that JMeter’s saveservice.properties maps to the setUp and tearDown Thread Group classes.

How many users one load generator can hold

The official documentation gives no number, and none would be right for long: the answer depends on the hardware, the plan and the server. Best-practices names the factors: the generator’s hardware, the test plan design, and the speed of the server, since a faster server returns responses sooner and so makes JMeter do more work. It warns that a wrongly sized number of threads leads to coordinated omission: when the generator cannot keep up, requests are sent late, the delay is not recorded, and response times look better than they are.

What a thread costs:

  • Memory. Each thread is a Java thread with its own stack and its own clone of the elements under its Thread Group, plus its variables, cookies, cache entries and the response data currently held. Large CSV sets, big responses kept for extractors and listeners that keep samples in memory (View Results Tree, the table and graph listeners) multiply that cost. JMeter’s heap is set with HEAP (default -Xms1g -Xmx1g -XX:MaxMetaspaceSize=256m), as described on Installation & Configuration.

  • CPU. Mostly proportional to requests per second, not to threads: TLS handshakes, compression, parsing for extractors and assertions (XPath and regular expressions on big bodies are the expensive ones), JSR223 scripts and writing results. A thread sleeping in a timer costs almost nothing, so 1,000 threads with a 10 s think time can be cheaper than 50 threads with none.

  • Network. Bandwidth and ephemeral ports on the generator, and connection reuse (see Same user on each iteration).

A practical way to find the limit of one machine:

  1. Run the plan in CLI mode with the lean settings from best-practices: no GUI listeners, CSV results, few assertions.

  2. Point it at a target that is known to be fast, or use the Java Request sampler’s JavaTest class, which needs no network and gives an idea of the maximum throughput achievable on that platform.

  3. Increase -Jthreads in steps and watch the generator’s CPU, heap and garbage collection (for example with the JDK’s jcmd or jstat) alongside the summariser’s rate.

  4. The limit is reached when the generator’s CPU approaches saturation, GC pauses grow, or the rate stops rising while the server is not the bottleneck. Stay well below it for real tests and keep the generator’s metrics in the report.

for threads in 100 200 400 800; do
  HEAP="-Xms2g -Xmx2g" jmeter -n -t bookshelf-load.jmx -l "calib-$threads.jtl" \
    -Jthreads=$threads -Jrampup=$((threads / 10)) -Jloops=-1 -Jduration=300
done

When one machine is not enough, run several generators, either in JMeter’s distributed mode or as independent CLI instances whose result files are merged afterwards; both are described in the distributed testing page of this section. Delay Thread creation until needed helps only when many threads are short-lived; it does not raise the number of threads that can be active at once.

Pitfalls

  • Duration shorter than expected. The duration includes the ramp-up; set it to ramp-up plus hold.

  • Analysing the ramp-up. Results from the ramp-up mix light and full load. Report the hold period separately (the HTML dashboard and listeners can filter by time).

  • A zero or tiny ramp-up starts every thread at once and measures a login spike, not a load test.

  • Loop count with a long ramp-up never reaches full concurrency; use Infinite with a duration.

  • Stop Thread on error shrinks the population; Stop Test ends a load test on the first timeout. Prefer Continue or Start Next Thread Loop for load, Stop Test for smoke and setUp.

  • No timeouts on samplers lets a hung server delay the end of the test indefinitely, because the end time is only checked between samples.

  • Same user on each iteration left at its default in a plan that models new visitors under-measures connection and TLS set-up; changed between two runs, it makes them incomparable.

  • Expecting tearDown after a Stop. tearDown never runs after a Stop, and after a Shutdown only with the Test Plan option selected.

  • Forgetting that properties are per generator. With -J in a distributed run the remote servers use the defaults; use -G.

  • Treating threads as requests per second. A Thread Group fixes concurrency, not throughput; when the target is a rate, use the open model.

From the books to 5.6 to 6.0

Topic Books (JMeter 2.12 and 3.0: PTwJ ch. 2, Pro ch. 5) JMeter 5.6.3 and planned 6.0

Core fields

Number of threads, ramp-up period, loop count, action on sampler error.

Unchanged in meaning and in their .jmx properties.

Scheduler

A Scheduler check box with absolute Start Time and End Time plus Duration and Startup delay.

Start and End Time removed in 4.0; the check box is now Specify Thread lifetime with Duration and Startup delay only.

Thread creation

Delay Thread creation until needed (added in 2.8) available.

Unchanged.

Iteration semantics

Cookies and connections controlled only by the Cookie and Cache Managers and HttpClient properties.

Same user on each iteration (5.2) controls them from the Thread Group.

Running and checking groups

Run the whole plan.

Run a selection of groups and Validate mode (both 3.0).

Closed vs open model

Only thread-based groups in core; arrival-rate shaping through plugins.

Core adds the Open Model Thread Group (5.5, experimental); the Thread Group itself stays the closed model.

JMeter 6.0 (planned)

Not applicable.

The changes.xml file on the master branch lists no change to the Thread Group’s fields. The thread-group-related change planned there is that the Open Model Thread Group (and the Precise Throughput Timer) will compute delays from the start of the Thread Group instead of the start of the test. 6.0 is planned to require Java 17.

Source: Changes History

References