Custom Thread Groups & Load Shaping

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 (Java 8 or later, Java 17 or later recommended; the examples assume Java 17 or 21), and against two plugin packages from the community catalogue at jmeter-plugins.org, installed with the JMeter Plugins Manager 2.0:

  • Custom Thread Groups, plugin id jpgc-casutg, version 3.1.1: the Concurrency, Ultimate, Stepping, Arrivals and Free-Form Arrivals Thread Groups.

  • Throughput Shaping Timer, plugin id jpgc-tst, version 2.6, which also provides the __tstFeedback function.

JMeter 6.0 is not released. Its planned changes (per xdocs/changes.xml on the apache/jmeter master branch) require Java 17 or later, with Java 21 recommended, and none of them names these plugins; whether jpgc-casutg 3.1.1 and jpgc-tst 2.6 load unchanged in 6.0 is not stated anywhere official, so re-test plans that use them after upgrading. jmeter-plugins.org is an independent community project, not part of Apache JMeter. Its wiki pages describe these elements only briefly, so where the wiki is silent (field labels, JMX property names, how threads are started and stopped) this page states what the plugin source code in the undera/jmeter-plugins repository does, and says so.

The core Thread Group has one shape: a linear ramp-up to a fixed number of threads, then a hold. The Open Model Thread Group, added in JMeter 5.5, schedules arrivals instead of threads. Before either existed, testers used plugins to draw other shapes: staircases to find the point where a system saturates, sudden spikes, waves, and a fixed number of requests per second regardless of response time. Those plugins are still maintained and still widely used, and this page covers them in full: what each element does, every field, the properties that let one plan run several profiles from the command line, the cost on the load generator, and how each compares with the core elements. It ends with the Bookshelf stress (steps to saturation) and spike profiles and a comparison table.

The core concepts are on earlier pages: the Thread Group fields and the ramp-up maths on Thread Groups: Concurrent Users & Ramp-up, the closed and open workload models, the Open Model Thread Group and the core throughput timers on Open Model & Throughput Control, timer scoping and the Synchronizing Timer on Timers & Think Time, and __P on Properties, Variables & Functions. Installing plugins is covered on Plugins & Plugins Manager.

Installing the two plugins

In the GUI, open Options → Plugins Manager, select Custom Thread Groups and Throughput Shaping Timer on the Available Plugins tab and click Apply Changes and Restart JMeter. For a build server or a container image, use the command-line installer described on the Plugins Manager wiki (it needs cmdrunner in lib/, version 2.3 for Plugins Manager 2.0 although the wiki still names cmdrunner-2.0.jar, and the PluginsManagerCMD script in bin/, which PluginManagerCMDInstaller creates):

bin/PluginsManagerCMD.sh install jpgc-casutg=3.1.1,jpgc-tst=2.6
bin/PluginsManagerCMD.sh status

status prints the installed plugin ids with their versions, a list such as [jpgc-casutg=3.1.1, jpgc-tst=2.6, jmeter-core=5.6.3, ...]. A plan that already uses the elements can also install exactly what it needs with install-for-jmx:

bin/PluginsManagerCMD.sh install-for-jmx bookshelf-load.jmx

After a restart the thread groups appear under Add → Threads (Users) with a prefix that marks their origin: bzm - Concurrency Thread Group, bzm - Arrivals Thread Group and bzm - Free-Form Arrivals Thread Group (contributed by BlazeMeter), and jp@gc - Ultimate Thread Group and jp@gc - Stepping Thread Group. The timer appears under Add → Timer → jp@gc - Throughput Shaping Timer. Every load generator that runs the plan needs the same plugins, including every remote server in a distributed run.

Two families of custom thread groups

The five thread groups in jpgc-casutg come from two code bases, and the difference matters more than the names suggest.

"jp@gc" schedulers: Ultimate, Stepping "bzm" dynamic groups: Concurrency, Arrivals, Free-Form Arrivals

How threads are created

All threads of the group are created when the test starts, each with its own start and end time, exactly like the core Thread Group with a scheduler. Threads sleep until their start time.

Threads are created on demand by a background thread starter while the test runs. The wiki says of the Concurrency Thread Group that it "won’t create all the threads upfront, so extra memory won’t be used".

How threads end

At their scheduled end time. As in the core Thread Group, the end time is checked between samples, so an iteration can be cut after any sampler.

At the end of an iteration, when the schedule is over or the target has dropped. The wiki calls the Concurrency Thread Group a replacement for Stepping because "it allows threads to finish their job gracefully".

Loop

An implicit Loop Controller set to loop forever; the thread runs until its end time.

An implicit Virtual User Controller (com.blazemeter.jmeter.control.VirtualUserController), which also handles the iteration limit, the shrinking of concurrency and the pooling of Arrivals threads.

Run-time changes

The schedule is fixed when the group starts.

The fields are re-read during the run (every 20 ms by default), so a field that reads a JMeter property follows that property if it changes.

Preview

A chart of the expected number of active threads.

A chart that redraws as you type: concurrency for the Concurrency Thread Group, arrivals per time unit for the two Arrivals groups.

The "how threads are created and end" rows describe the 3.1.1 source code (AbstractSimpleThreadGroup for the jp@gc groups, AbstractDynamicThreadGroup, the thread starters and VirtualUserController for the bzm groups); the quoted sentences are from the Concurrency Thread Group wiki page.

Concurrency Thread Group

The Concurrency Thread Group is the plugin most plans use today. Its wiki describes it as offering a "simplified approach for configuring threads schedule", intended to maintain a level of concurrency: it starts additional threads during the run whenever fewer than the target are running in parallel. It is a closed-model group like the core Thread Group (each thread waits for its response before the next request), but with steps, a separate hold time and graceful thread endings. Menu path: right-click the Test Plan, Add → Threads (Users) → bzm - Concurrency Thread Group.

Fields

The panel has the shared Action to be taken after a Sampler error section at the top (the same five choices as the core Thread Group, stored in ThreadGroup.on_sample_error), then the load fields, the preview chart and the additional fields. Labels and property names below are those of the 3.1.1 source; the time labels show (sec) or (min) according to the Time Unit.

GUI field JMX property Meaning

Target Concurrency

TargetLevel

The number of threads to keep running at the end of the ramp-up and during the hold. It is not multiplied by the time unit. Accepts functions, for example ${__P(threads,50)} or ${__tstFeedback(...)}.

Ramp Up Time (sec or min)

RampUp

Time to grow from 0 to the target. Empty or 0 means all threads start at once.

Ramp-Up Steps Count

Steps

Empty or 0: a linear ramp. A number n: the ramp is a staircase of n equal steps of target / n threads, each lasting ramp-up / n.

Hold Target Rate Time (sec or min)

Hold

How long to keep the target after the ramp-up has ended. The group’s total run time is ramp-up plus hold.

Time Unit: minutes / seconds

Unit (M or S)

Unit of the two time fields. The 3.1.1 GUI pre-selects minutes for a new element; easy to miss when you meant seconds.

Thread Iterations Limit

Iterations

Empty: unlimited. A number: each thread stops after that many iterations and is replaced by a new thread if the target still requires it.

Same User On Each Iteration

same_user_on_next_iteration

Same meaning as the core Thread Group option: selected (default), cookies, cache and connections carry over to the next iteration. Added in jpgc-casutg 3.0.

Log Threads Status into File

LogFilename

Optional file name. The group writes a record for every thread start and finish (the markers START and FINISH in the response message column), which is useful to check what the group actually did.

How the target is computed

The 3.1.1 thread starter computes the planned concurrency \(C(t)\) at elapsed time \(t\), with target \(N\), ramp-up \(R\), hold \(H\) and \(s\) steps:

\[ C(t) = \begin{cases} \operatorname{round}\!\left(\dfrac{N}{s}\left(\left\lfloor \dfrac{t}{R/s} \right\rfloor + 1\right)\right) & 0 \le t \le R,\ s > 0\\[2ex] \operatorname{round}\!\left(\dfrac{N}{R}\,t\right) & 0 \le t \le R,\ s = 0\\[2ex] N & R < t < R + H\\[1ex] \text{stop} & t \ge R + H \end{cases} \]

Three consequences are worth knowing:

  • With steps, the first step starts immediately and the last step starts one step early. With \(N = 200\), \(R = 20\) minutes and \(s = 10\), the group runs 20 threads from minute 0, 40 from minute 2, and reaches 200 at minute 18, not at minute 20. Each level therefore lasts exactly \(R/s\), and the top level lasts \(R/s + H\).

  • The hold comes after the ramp-up. Unlike the core Thread Group, whose Duration includes the ramp-up, the Concurrency Thread Group runs for \(R + H\). The section’s standard load profile (50 threads, 60 s ramp-up, 10-minute hold) is Ramp Up Time 60 and Hold Target Rate Time 600 in seconds, where the core Thread Group needs a duration of 660.

  • Concurrency falls gracefully. When a thread finishes an iteration and there are more threads than \(C(t)\) requires (because the target was lowered or the hold is over), that thread ends. In-flight iterations are never cut. There is no ramp-down field: at \(R + H\) every thread ends at the end of its current iteration.

The starter checks every second (or as soon as a thread finishes) whether more threads are needed, and starts them. Threads that end because of an error action, an iteration limit or a failed login are therefore replaced, which is what "maintain the level of concurrency" means.

Configurable properties

The wiki lists three JMeter properties that tune all the bzm dynamic groups. Set them in user.properties or with -J.

Property Default Meaning (from the wiki)

dynamic_tg.properties_caching_validity

20 (ms)

For performance the group caches its field values this long. Set it negative if the values never change during the run; lower it to react faster, at a CPU cost.

dynamic_tg.temporisation

10 (ms)

A sleep added after all threads stopped.

dynamic_tg.shift_rampup_start

0

Shifts the start of the ramp-up to avoid a CPU spike at the very beginning, when no thread exists yet. The wiki gives the default as 0 millis; the 3.1.1 starter subtracts the value from the elapsed time in seconds, so treat the unit as seconds.

Fragment: the Bookshelf stress profile

The Bookshelf stress profile climbs from 20 to 200 threads in ten steps of two minutes and then holds for five minutes, to find the concurrency at which the API saturates. The same journey as the load profile runs under the group (log in, search, view a book, create a loan, view the loan, with 3 s of think time). The fields read properties so that the same plan runs smaller steps in a test environment. Time Unit is set to seconds so that rampup keeps the meaning it has in the rest of the section.

<com.blazemeter.jmeter.threads.concurrency.ConcurrencyThreadGroup
    guiclass="com.blazemeter.jmeter.threads.concurrency.ConcurrencyThreadGroupGui"
    testclass="com.blazemeter.jmeter.threads.concurrency.ConcurrencyThreadGroup" testname="Stress to saturation">
  <elementProp name="ThreadGroup.main_controller" elementType="com.blazemeter.jmeter.control.VirtualUserController"/>
  <stringProp name="ThreadGroup.on_sample_error">startnextloop</stringProp>
  <stringProp name="TargetLevel">${__P(threads,200)}</stringProp>
  <stringProp name="RampUp">${__P(rampup,1200)}</stringProp>
  <stringProp name="Steps">${__P(steps,10)}</stringProp>
  <stringProp name="Hold">${__P(hold,300)}</stringProp>
  <stringProp name="LogFilename"></stringProp>
  <stringProp name="Iterations"></stringProp>
  <stringProp name="Unit">S</stringProp>
  <boolProp name="same_user_on_next_iteration">false</boolProp>
</com.blazemeter.jmeter.threads.concurrency.ConcurrencyThreadGroup>
<hashTree>
  <!-- CSV Data Set Config, Cookie Manager, the five-request journey and its think time go here -->
</hashTree>

The element and property names are those written by the 3.1.1 GUI (the shape matches the demo.jmx shipped with the Throughput Shaping Timer sources). Save the plan from the GUI to get the exact form your installation writes.

jmeter -n -t bookshelf-load.jmx -l stress.jtl -e -o report-stress \
  -Jthreads=200 -Jrampup=1200 -Jsteps=10 -Jhold=300

Worked numbers. One Bookshelf thread sends a request every \(R_t + Z = 0.2 + 3 = 3.2\) s, that is 0.3125 requests per second, as long as the response time stays at 0.2 s. If the server kept up, each step would add \(20 \times 0.3125 = 6.25\) requests per second: 6.25 at 20 threads, 31.25 at 100, 62.5 at 200. The test runs \(1200 + 300 = 1500\) s (25 minutes). Saturation shows as the step at which throughput stops rising while response times climb. Suppose the summariser reports about 30 requests per second at 120 threads and still about 30 at 140. Little’s Law, \(R_t = N/X - Z\), then gives \(120 / 30 - 3 = 1.0\) s and \(140 / 30 - 3 \approx 1.67\) s: the extra threads only queue. The capacity is about 30 requests per second, reached between 100 and 120 threads.

A representative summariser excerpt (the numbers depend on the system under test; each 30 s line falls inside one two-minute step):

summary +    188 in 00:00:30 =    6.3/s Avg:   201 Min:    38 Max:   655 Err:     0 (0.00%) Active: 20 Started: 20 Finished: 0
...
summary +    934 in 00:00:30 =   31.1/s Avg:   212 Min:    41 Max:   980 Err:     0 (0.00%) Active: 100 Started: 100 Finished: 0
...
summary +    903 in 00:00:30 =   30.1/s Avg:   998 Min:    44 Max:  4120 Err:     0 (0.00%) Active: 120 Started: 120 Finished: 0
...
summary +    897 in 00:00:30 =   29.9/s Avg:  1672 Min:    52 Max:  6904 Err:     3 (0.33%) Active: 140 Started: 140 Finished: 0

In jmeter.log the group’s thread starter logs Start supplying threads when it begins and Done supplying threads when the schedule is over.

Ultimate Thread Group

The Ultimate Thread Group draws a load profile as a table of independent rows. Its wiki lists the features as an unlimited number of schedule records, a separate ramp-up time, shutdown time and flight time for each record, and a load preview graph. Each row describes one batch of threads; rows overlap freely, so the active-thread count at any moment is the sum of all rows active at that moment. That is what makes spikes and waves easy: a baseline row plus short rows on top of it. Menu path: Add → Threads (Users) → jp@gc - Ultimate Thread Group. Click Add Row to append a record; Copy Row and Delete Row work on the selected row.

Fields

The Threads Schedule table has five columns. All times are in seconds (whole numbers).

Column Meaning

Start Threads Count

The number of threads in this row.

Initial Delay, sec

Delay from the start of the thread group to the beginning of this row’s ramp-up. Every row counts from the group start; rows are not chained.

Startup Time, sec

The row’s ramp-up: its threads start evenly over this time.

Hold Load For, sec

How long all the row’s threads stay active once the startup time is over (the "flight time").

Shutdown Time

The row’s ramp-down, in seconds: its threads reach their end times evenly over this time.

The panel also has the shared Action to be taken after a Sampler error section. The table is stored as a collectionProp named ultimatethreadgroupdata, one inner collectionProp per row holding five stringProp values in column order. New rows are pre-filled with 100, 0, 30, 60 and 10 by the 3.1.1 GUI.

In 3.1.1 each thread of a row gets a start time of group start + initial delay + its share of the startup time, and an end time of start of hold + hold + its share of the shutdown time. The row lasts initial delay + startup + hold
shutdown in total. A thread at its end time stops between two samples, so with a multi-request journey some iterations are cut part way; leave a few seconds of shutdown time so that the cut is spread out rather than simultaneous.

The threads_schedule property

The wiki documents a way to replace the table from a JMeter property, in user.properties or with -J. The schedule is a list of spawn directives:

spawn(threadsCount, initialDelay, startupTime, holdLoadFor, shutdownTime)

The four durations accept case-insensitive suffixes s, m, h and d, combinable as in 1d11h23m6s. The wiki’s example is threads_schedule=spawn(15,1s,1s,1s,1s) spawn(40,1s,3s,1s,2s).

When the property is set and not empty, the 3.1.1 source logs GUI threads profile will be ignored and uses the property instead of the table. The property name is fixed, so it overrides every Ultimate Thread Group in the plan; a plan with two Ultimate groups cannot give them different schedules this way. A directive that cannot be parsed is skipped with a warning in the log, not reported as an error, so check the log line Setting threads profile from property threads_schedule: ... and the thread count of the start-up line (Starting thread group number 1 threads 350) after changing it.

Fragment: the Bookshelf spike profile

The Bookshelf spike profile keeps the 50-thread baseline for about 20 minutes and fires two spikes of 150 extra threads, each reaching 200 threads in 10 s and holding for a minute. Two spikes, eight minutes apart, show whether the API recovers between them (response times back to the baseline, connection pools drained, no growing error rate) and whether the second spike is worse than the first.

Start Threads Count Initial Delay, sec Startup Time, sec Hold Load For, sec Shutdown Time Active

50

0

60

1140

30

0 to 60 s ramp, 50 threads from 60 to 1200 s, down to 0 at 1230 s

150

300

10

60

10

+150 from 300 to 310 s, held to 370 s, gone at 380 s

150

780

10

60

10

+150 from 780 to 790 s, held to 850 s, gone at 860 s

<kg.apc.jmeter.threads.UltimateThreadGroup
    guiclass="kg.apc.jmeter.threads.UltimateThreadGroupGui"
    testclass="kg.apc.jmeter.threads.UltimateThreadGroup" testname="Spike">
  <collectionProp name="ultimatethreadgroupdata">
    <collectionProp name="1">
      <stringProp name="1691">50</stringProp>
      <stringProp name="48">0</stringProp>
      <stringProp name="1722">60</stringProp>
      <stringProp name="1508508">1140</stringProp>
      <stringProp name="1629">30</stringProp>
    </collectionProp>
    <collectionProp name="2">
      <stringProp name="48780">150</stringProp>
      <stringProp name="50547">300</stringProp>
      <stringProp name="1567">10</stringProp>
      <stringProp name="1722">60</stringProp>
      <stringProp name="1567">10</stringProp>
    </collectionProp>
    <collectionProp name="3">
      <stringProp name="48780">150</stringProp>
      <stringProp name="54639">780</stringProp>
      <stringProp name="1567">10</stringProp>
      <stringProp name="1722">60</stringProp>
      <stringProp name="1567">10</stringProp>
    </collectionProp>
  </collectionProp>
  <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller">
    <boolProp name="LoopController.continue_forever">false</boolProp>
    <intProp name="LoopController.loops">-1</intProp>
  </elementProp>
  <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
</kg.apc.jmeter.threads.UltimateThreadGroup>
<hashTree/>

The numeric name attributes of the inner properties are generated by the GUI (for the values, they are the Java string hash codes of the cell text) and carry no meaning. The same schedule as a property, for a CLI run that varies the spike size without editing the plan:

jmeter -n -t bookshelf-spike.jmx -l spike.jtl -e -o report-spike \
  -J "threads_schedule=spawn(50,0s,1m,19m,30s) spawn(150,5m,10s,1m,10s) spawn(150,13m,10s,1m,10s)"

Worked numbers. Peak concurrency is \(50 + 150 = 200\) threads, four times the baseline, reached in 10 s, which is a start rate of 15 threads per second, against 0.83 per second during the baseline ramp-up. Each spike holds 200 threads for 60 s; at the light-load rate of 0.3125 requests per second per thread that would be 62.5 requests per second, but a spike exists to show that the server cannot keep that rate, so expect response times to jump and the measured throughput to stay well below it. Note that the group creates 350 threads at the start of the test (the sum of all rows), even though at most 200 are active at once: threads are created up front and sleep until their start time.

Spikes: Ultimate Thread Group or Synchronizing Timer?

Both are called "spike" tools, but they spike different things. The Ultimate Thread Group adds users: the extra threads run full journeys for as long as the row holds them. The core Synchronizing Timer, described on Timers & Think Time, holds existing threads until a number of them have gathered and then releases them at the same instant, a burst of simultaneous requests to one sampler with no extra users. Use the Ultimate Thread Group for "the audience triples during the lunchtime newsletter"; use the Synchronizing Timer for "500 users press Borrow the second a rare book becomes available". They combine: a Synchronizing Timer on the loan request under the spike rows produces both.

Arrivals Thread Group

The Arrivals Thread Group expresses load as arrivals per time unit. The wiki defines an arrival as the start of a thread iteration, and says the group "will create new threads if all existing threads are busy in the middle of iterations". It is the plugin form of the open model: the rate of new iterations is the input, and the concurrency grows when the server slows down. The wiki warns that a constant arrival rate means increasing concurrency, and that the Concurrency Limit field is a "safety valve to prevent running out of memory". Menu path: Add → Threads (Users) → bzm - Arrivals Thread Group.

Fields

The load fields are the same as the Concurrency Thread Group’s, with a rate instead of a thread count; the additional fields add the concurrency limit.

GUI field JMX property Meaning

Target Rate (arrivals/sec or arrivals/min)

TargetLevel

Arrivals per time unit at the end of the ramp-up and during the hold. With Time Unit minutes the value is per minute: 240 arrivals/min is 4 per second.

Ramp Up Time (sec or min)

RampUp

Time to grow the rate from 0 to the target.

Ramp-Up Steps Count

Steps

Empty or 0: a linear ramp of the rate. n: n equal rate steps, the first starting immediately, as for the Concurrency Thread Group.

Hold Target Rate Time (sec or min)

Hold

How long to hold the target rate after the ramp-up. Arrivals stop at ramp-up + hold.

Time Unit: minutes / seconds

Unit (M or S)

Unit of the rate and the two times. Minutes are pre-selected for a new element.

Thread Iterations Limit

Iterations

Empty: unlimited. A number: a thread ends after that many iterations instead of returning to the pool.

Same User On Each Iteration

same_user_on_next_iteration

As for the core Thread Group (3.0 and later). It matters more here, because threads are reused across arrivals.

Log Threads Status into File

LogFilename

Writes ARRIVAL, COMPLETION and ABANDONMENT records for every iteration.

Concurrency Limit

ConcurrencyLimit

Maximum number of threads busy at once. Empty or 0 means no limit. The GUI pre-fills 1000.

How arrivals are scheduled

The wiki only gives the definition; the 3.1.1 source (ArrivalsThreadStarter) shows four behaviours that shape the results:

  • Arrivals are evenly spaced. The starter schedules the next arrival \(1000 / r\) ms after the previous one, where \(r\) is the current rate per second. At 4 arrivals per second an iteration starts every 250 ms exactly. This differs from the Open Model Thread Group, whose random_arrivals are random, and it hides the bursts that real independent arrivals contain.

  • Threads are pooled and reused. When a thread finishes an iteration it waits in a pool; the next arrival takes a pooled thread before a new one is created. The pool keeps the thread count close to rate × iteration time and saves the cost of creating a thread and cloning the test tree per arrival. Because a pooled thread keeps its variables, a journey that stores a token in a variable sees the previous arrival’s value unless the journey sets it again; clear Same User On Each Iteration if each arrival must be a new user for cookies and connections.

  • The concurrency limit drops arrivals. When the limit is reached and no pooled thread is free, the arrival is skipped, not queued. The rate then falls below the target and the run looks healthier than it is; compare the Done N arrivals line that the group logs at the end of the test with the expected count.

  • The end is graceful. After ramp-up + hold no new arrivals start; running iterations complete.

Fragment: 20 requests per second with a safety valve

The section’s arrival-rate target, 20 requests per second from the five-request Bookshelf journey, is 4 arrivals per second, as on Open Model & Throughput Control. One journey takes about 5 s (five requests of 0.2 s and four 1 s think times), so on average \(4 \times 5 = 20\) threads are busy. With 5 s response timeouts the worst journey is 29 s, so a stall could need up to \(4 \times 29 = 116\) threads: a Concurrency Limit of 120 bounds the generator without dropping arrivals in the worst case the timeouts allow.

<com.blazemeter.jmeter.threads.arrivals.ArrivalsThreadGroup
    guiclass="com.blazemeter.jmeter.threads.arrivals.ArrivalsThreadGroupGui"
    testclass="com.blazemeter.jmeter.threads.arrivals.ArrivalsThreadGroup" testname="Borrower arrivals">
  <elementProp name="ThreadGroup.main_controller" elementType="com.blazemeter.jmeter.control.VirtualUserController"/>
  <stringProp name="ThreadGroup.on_sample_error">startnextloop</stringProp>
  <stringProp name="TargetLevel">${__P(arrivals,4)}</stringProp>
  <stringProp name="RampUp">60</stringProp>
  <stringProp name="Steps"></stringProp>
  <stringProp name="Hold">600</stringProp>
  <stringProp name="LogFilename"></stringProp>
  <stringProp name="Iterations"></stringProp>
  <stringProp name="Unit">S</stringProp>
  <boolProp name="same_user_on_next_iteration">false</boolProp>
  <stringProp name="ConcurrencyLimit">120</stringProp>
</com.blazemeter.jmeter.threads.arrivals.ArrivalsThreadGroup>
<hashTree/>

Over the run: \(4 \times 60 / 2 = 120\) arrivals during the linear ramp and \(4 \times 600 = 2400\) during the hold, about 2520 journeys and 12,600 requests. The plugin’s preview chart shows the same total in its legend (Arrival Rate (~2520 total arrivals)). At the end of the test jmeter.log holds a line of the form Done 2520 arrivals, 2518 completions, 2 abandonments. A noticeably smaller arrival count means arrivals were dropped by the concurrency limit; abandonments are iterations ended early by Start Next Thread Loop.

Free-Form Arrivals Thread Group

The Free-Form Arrivals Thread Group "has the same idea of arriving users" as the Arrivals Thread Group and "offers free-form schedule capabilities" (wiki). Instead of ramp, steps and hold, it has a Threads Schedule table of segments, each a linear change of the arrival rate. Menu path: Add → Threads (Users) → bzm - Free-Form Arrivals Thread Group.

Field Meaning

Threads Schedule: Start Value

Arrival rate at the start of the segment, per time unit.

Threads Schedule: End Value

Arrival rate at the end of the segment; equal to Start Value for a constant segment.

Threads Schedule: Duration

Segment length in the time unit. Segments run one after the other; the schedule ends after the last one.

Time Unit, Thread Iterations Limit, Same User On Each Iteration, Log Threads Status into File, Concurrency Limit

As for the Arrivals Thread Group.

The table is stored as a collectionProp named Schedule. New rows default to 1, 10 and 60 in the 3.1.1 GUI. A segment that starts at 0 means no arrivals for a while: in 3.1.1 the starter polls every 200 ms while the rate is 0, so a 0-to-something segment starts with a short silence rather than a fraction of an arrival.

The section’s arrival-rate profile, from 5 to 20 arrivals per second over five minutes and then 20 per second for ten minutes, is two rows with Time Unit set to seconds:

Start Value End Value Duration Arrivals

5

20

300

\((5 + 20) / 2 \times 300 = 3750\)

20

20

600

\(20 \times 600 = 12000\)

<com.blazemeter.jmeter.threads.arrivals.FreeFormArrivalsThreadGroup
    guiclass="com.blazemeter.jmeter.threads.arrivals.FreeFormArrivalsThreadGroupGui"
    testclass="com.blazemeter.jmeter.threads.arrivals.FreeFormArrivalsThreadGroup" testname="Search arrivals">
  <elementProp name="ThreadGroup.main_controller" elementType="com.blazemeter.jmeter.control.VirtualUserController"/>
  <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
  <collectionProp name="Schedule">
    <collectionProp name="1">
      <stringProp name="53">5</stringProp>
      <stringProp name="1598">20</stringProp>
      <stringProp name="50547">300</stringProp>
    </collectionProp>
    <collectionProp name="2">
      <stringProp name="1598">20</stringProp>
      <stringProp name="1598">20</stringProp>
      <stringProp name="53430">600</stringProp>
    </collectionProp>
  </collectionProp>
  <stringProp name="LogFilename"></stringProp>
  <stringProp name="Iterations"></stringProp>
  <stringProp name="Unit">S</stringProp>
  <boolProp name="same_user_on_next_iteration">true</boolProp>
  <stringProp name="ConcurrencyLimit">100</stringProp>
</com.blazemeter.jmeter.threads.arrivals.FreeFormArrivalsThreadGroup>
<hashTree/>

With one search request per arrival (0.2 s), the 15,750 arrivals need on average \(20 \times 0.2 = 4\) busy threads at full rate; the limit of 100 covers a 5 s response timeout at 20 per second. Compared with the Open Model Thread Group schedule rate(5/sec) random_arrivals(5 min) rate(20/sec) random_arrivals(10 min), the total is the same, but the arrivals are evenly spaced, threads are pooled, and the group itself caps the concurrency.

Stepping Thread Group (legacy)

The Stepping Thread Group was, in its wiki’s words, the first custom thread group, written when JMeter 2.4 introduced the plugin type, to give LoadRunner-style stepped scheduling. The same page now points to the Concurrency Thread Group as a "better alternative". It is still shipped in jpgc-casutg (version 2.10 removed its deprecation flag), so old plans keep loading, and JMeter Cookbook chapter 6 uses it. For new plans, prefer the Concurrency Thread Group. Menu path: Add → Threads (Users) → jp@gc - Stepping Thread Group.

The Threads Scheduling Parameters panel reads as a sentence. Field labels and properties are from the 3.1.1 source; all times are in seconds.

Field (the sentence) JMX property Meaning

This group will start N threads

ThreadGroup.num_threads

Total number of threads.

First, wait for N seconds

Threads initial delay

Delay before the first thread starts.

Then start N threads

Start users count burst

An initial burst. 0 means the first step is a normal step.

Next, add N threads every N seconds, using ramp-up N seconds

Start users count, Start users period, rampUp

Each step adds this many threads, spreading their start over the ramp-up; the next step begins period seconds after the previous one’s ramp-up ended.

Then hold load for N seconds

flighttime

Hold after the last thread has started.

Finally, stop N threads every N seconds

Stop users count, Stop users period

Ramp-down in portions.

The wiki’s example adds 10 users every 2 minutes. A Concurrency Thread Group with Target Concurrency 100, Ramp Up Time 20 minutes and Ramp-Up Steps Count 10 produces the same staircase. The step ramp-up and the stepwise shutdown have no direct equivalent there; if they matter, use an Ultimate Thread Group with one row per step. Like the Ultimate Thread Group, the Stepping Thread Group creates all its threads at the start and stops each at its end time, between samples.

Throughput Shaping Timer

The Throughput Shaping Timer sets a schedule of requests per second and delays threads so that the samplers in its scope do not exceed it. Its wiki presents it as the way out of the closed-workload loop of tuning threads and timers until the right RPS comes out, and as giving the benefits of the open approach. That needs a careful reading: it is a timer. In its own words, "JMeter timers can only delay threads (and limit RPS)", so the threads must be able to produce more than the schedule, and if they cannot, the rate falls short. Menu path: on the thread group or on a sampler, Add → Timer → jp@gc - Throughput Shaping Timer.

Fields and placement

The panel has one table, Requests Per Second (RPS) Schedule, with Add Row, Copy Row and Delete Row buttons and a preview chart (Expected RPS).

Column Meaning

Start RPS

Requests per second at the start of the segment.

End RPS

Requests per second at the end of the segment; the rate changes linearly between the two.

Duration, sec

Segment length in seconds. Segments run one after the other.

The table is stored as a collectionProp named load_profile. Like every timer, the Throughput Shaping Timer applies to every sampler in its scope and counts all their samples: under a thread group with a five-request journey, 20 RPS means 20 HTTP samples per second in total (4 journeys), and the delay falls before each of the five requests. Put it as the child of a single sampler to shape only that request. In the 2.6 source the timer is one shared instance for all threads (it implements NoThreadClone), and other timers in scope still add their delays, so either remove think time from a shaped scope or size the threads for both.

Notes from the wiki

The wiki lists these rules:

  • Threads of the thread groups in the timer’s scope are stopped when the RPS schedule finishes. The schedule is therefore also the test duration.

  • Provide enough threads; the timer can only delay them.

  • With JMeter versions lower than 3.3, if the RPS decreases at the end of the test, decrease the threads too, otherwise there is a spike in the last second.

  • Avoid zero RPS at the start of the test, which also produces a spike, and avoid zero RPS during the test.

When the timer runs short of threads, the 2.6 source logs a warning every second of the form No free threads available in current Thread Group Search, made 31 samples/s for expected rps 40 samples/s, increase your number of threads. When the schedule ends it logs No further RPS schedule, asking threads to stop... and escalates over the next seconds from stopping the thread group gracefully to stopping the engine if threads do not end.

How many threads?

The wiki gives the sizing rule for a target rate:

\[ \text{threads} \approx \frac{\text{RPS} \times \text{max response time (ms)}}{1000} \]

Its example: a target of 1230 RPS with a response time of up to 2.5 s needs \(1230 \times 2500 / 1000 = 3075\) threads. It recommends some overhead for unexpected slow responses, but not too much, since excess threads lead to a "final spike". The rule is Little’s Law with the maximum response time instead of the average: the threads in flight are rate × time in flight, and sizing for the slowest expected response keeps the schedule fed when the server slows down.

For Bookshelf search at 40 RPS (the top of the stress steps below) with response times of up to 2 s: \(40 \times 2000 / 1000 = 80\) threads, so a maximum of about 100 leaves a 25 % margin. At the 20 RPS hold with the usual 0.2 s response time, only \(20 \times 0.2 = 4\) threads are busy on average; the rest wait in the timer.

load_profile properties: const, line and step

The schedule can come from a JMeter property instead of the table. The wiki defines three "functions":

Directive Meaning (from the wiki)

const(N, T)

Hold a constant load of N requests per second for T.

line(N, K, T)

Increase (or decrease) linearly from N to K requests per second over T.

step(N, K, S, T)

Step from N to K requests per second, each step S requests per second high and T long.

Durations take the suffixes s, m, h and d, combinable as 1d11h23m6s. Which property is used, in order of precedence:

  1. <timer name>.load_profile, for example search_tst.load_profile, applies to the timer with that name (support for several named properties was added in 2.6, per the plugin catalogue).

  2. load_profile applies to every Throughput Shaping Timer without a named property.

  3. If neither is set, the table from the GUI is used.

The wiki’s examples are my_timer.load_profile=const(10,10s) line(10,100,1m) step(5,25,5,1h) and load_profile=step(5, 50, 5, 2m) const(50, 10m). In the 2.6 source the RPS values in these directives are parsed as whole numbers; fractional rates (for example 0.5 RPS) can only be entered in the GUI table. step expands into one constant row per level, including both ends: step(25,40,5,2m) is four rows of 25, 30, 35 and 40 RPS, two minutes each. A directive that cannot be parsed is skipped with a warning in the log.

Properties exported by the timer

The wiki documents four properties that the timer updates every second, readable with __P or props.get(...) in a JSR223 element. elementName is the timer’s name.

Property Meaning

elementName_totalDuration

Total duration: the sum of the Duration, sec column.

elementName_cntDelayed

Number of threads currently waiting in the timer; below 1 means there are not enough threads to reach the RPS.

elementName_cntSent

Samples sent since the last second tick.

elementName_rps

Current target RPS.

For example, a JSR223 Sampler in a monitoring thread group can log props.get('search_tst_cntDelayed') every few seconds to show whether the schedule is starved.

The __tstFeedback function with the Concurrency Thread Group

Sizing for the maximum response time wastes threads for most of a run, and sizing for the average starves the schedule when the server slows down. The wiki’s answer is a feedback loop: write ${__tstFeedback(tst-name,1,100,10)} in the Target Concurrency field of a Concurrency Thread Group, and the group adds threads when the timer reports that the requested RPS is not met, and removes them when too many are idle.

Argument Name (function help) Meaning

1

Name of Throughput Shaping Timer to integrate with

The testname of the timer.

2

Starting concurrency

Threads at the start.

3

Max concurrency

Upper bound on the threads the function may request.

4

Spare threads ratio

Optional. Below 1: a ratio of the current estimate of needed threads; 1 or more: an absolute number of spare threads to keep.

The wiki’s instructions for the group: leave Ramp Up Time and Ramp-Up Steps Count empty, and set Hold Target Rate Time to at least the total duration of the timer’s schedule. In the 2.6 source the function keeps its estimate in the property <timer name>_concurrency, raises it in proportion to the shortfall when samples were missed in the last second with no thread waiting, lowers it when threads are waiting, never goes below 1 or above the maximum, and uses a spare ratio of 0.1 when the fourth argument is omitted.

<com.blazemeter.jmeter.threads.concurrency.ConcurrencyThreadGroup
    guiclass="com.blazemeter.jmeter.threads.concurrency.ConcurrencyThreadGroupGui"
    testclass="com.blazemeter.jmeter.threads.concurrency.ConcurrencyThreadGroup" testname="Search">
  <elementProp name="ThreadGroup.main_controller" elementType="com.blazemeter.jmeter.control.VirtualUserController"/>
  <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
  <stringProp name="TargetLevel">${__tstFeedback(search_tst,10,100,0.1)}</stringProp>
  <stringProp name="RampUp"></stringProp>
  <stringProp name="Steps"></stringProp>
  <stringProp name="Hold">1320</stringProp>
  <stringProp name="LogFilename"></stringProp>
  <stringProp name="Iterations"></stringProp>
  <stringProp name="Unit">S</stringProp>
  <boolProp name="same_user_on_next_iteration">true</boolProp>
</com.blazemeter.jmeter.threads.concurrency.ConcurrencyThreadGroup>
<hashTree>
  <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Search books">
    <stringProp name="HTTPSampler.domain">${__P(host,localhost)}</stringProp>
    <stringProp name="HTTPSampler.port">${__P(port,8080)}</stringProp>
    <stringProp name="HTTPSampler.protocol">http</stringProp>
    <stringProp name="HTTPSampler.path">/api/books?query=${term}</stringProp>
    <stringProp name="HTTPSampler.method">GET</stringProp>
    <stringProp name="HTTPSampler.connect_timeout">2000</stringProp>
    <stringProp name="HTTPSampler.response_timeout">5000</stringProp>
  </HTTPSamplerProxy>
  <hashTree/>
  <kg.apc.jmeter.timers.VariableThroughputTimer
      guiclass="kg.apc.jmeter.timers.VariableThroughputTimerGui"
      testclass="kg.apc.jmeter.timers.VariableThroughputTimer" testname="search_tst">
    <collectionProp name="load_profile">
      <collectionProp name="1">
        <stringProp name="49">1</stringProp>
        <stringProp name="1598">20</stringProp>
        <stringProp name="48687">120</stringProp>
      </collectionProp>
      <collectionProp name="2">
        <stringProp name="1598">20</stringProp>
        <stringProp name="1598">20</stringProp>
        <stringProp name="53430">600</stringProp>
      </collectionProp>
    </collectionProp>
  </kg.apc.jmeter.timers.VariableThroughputTimer>
  <hashTree/>
</hashTree>

The class names are those of the 2.6 timer (kg.apc.jmeter.timers.VariableThroughputTimer), as in the demo.jmx shipped with its sources, which uses the same pattern with ${__tstFeedback(ramp_up_tst,1,1000,2)}. The search-terms.csv CSV Data Set Config that provides term is omitted.

The Bookshelf RPS schedule, from the GUI and from a property

The table above is a warm-up line from 1 to 20 RPS over 2 minutes and a 10-minute hold at 20 RPS: \(21 \times 60 = 1260\) requests in the ramp (the average of 1 and 20 is 10.5 RPS over 120 s) plus 12,000 in the hold. To turn the same plan into an open-ish stress test, add RPS steps from the command line without touching the table:

jmeter -n -t bookshelf-search.jmx -l search-stress.jtl -e -o report-search-stress \
  -J "search_tst.load_profile=line(1,20,2m) const(20,10m) step(25,40,5,2m) line(40,5,1m)"

The named property wins over the table, so the timer runs 2 + 10 + 4 × 2 + 1 = 21 minutes (1260 s); the Concurrency Thread Group’s Hold of 1320 s covers it, as the wiki requires, and the timer stops the threads when its schedule ends. The steps hold 25, 30, 35 and 40 RPS for two minutes each, and the final line takes the rate down to 5 RPS so that the end is not a sudden drop. The figure below shows this schedule.

A representative summariser excerpt from the 30 RPS step, showing Active adjusted by the feedback function and the rate on target (numbers depend on the system):

summary +    901 in 00:00:30 =   30.0/s Avg:   214 Min:    37 Max:   903 Err:     0 (0.00%) Active: 12 Started: 41 Finished: 29

Started keeps growing while Active follows the need, because the Concurrency Thread Group ends surplus threads at the end of their iteration and starts new ones when the function raises the target.

The load profiles at a glance

The figure shows the three profiles built on this page: the Concurrency Thread Group stress staircase, the Ultimate Thread Group spikes, and the Throughput Shaping Timer schedule.

Load profiles: Concurrency Thread Group steps, Ultimate Thread Group spikes and a Throughput Shaping Timer RPS schedule

Load generator cost

Element Cost on the load generator

Concurrency Thread Group

Threads created on demand, at most the target (or what __tstFeedback requests). A background starter thread checks every second. Each new thread clones the group’s test tree, done in the background after a thread starts. Thread churn is low unless Thread Iterations Limit is small or the target changes often.

Ultimate Thread Group

All threads of all rows are created and their trees cloned when the group starts: the spike example creates 350 threads for a peak of 200. Rows that never overlap still cost memory for their whole lifetime. Keep rows few and large rather than many small ones.

Stepping Thread Group

As the Ultimate Thread Group: every thread created at the start.

Arrivals and Free-Form Arrivals

Threads created on demand and returned to a pool, so the count settles near rate × iteration time; unbounded without a Concurrency Limit. The pool avoids the per-arrival thread creation and tree cloning that the Open Model Thread Group does.

Throughput Shaping Timer

Negligible per sample, but every sampler in scope passes through one shared, synchronised delay method, and the threads waiting in it still exist. Over-provisioned threads cost memory and, at the end of a step down, can release a burst; the wiki’s "final spike" warning.

All of them are subject to the general rule from best-practices: the generator must not saturate. Watch its CPU, heap and garbage collection, especially during spikes, when the Ultimate Thread Group starts many threads in a few seconds.

Choosing a thread group

Element Model Input Shapes Threads End of run CLI parameterisation

Thread Group (core)

Closed

Threads, ramp-up, loops or duration

Linear ramp, hold

Created at start (or when needed with Delay Thread creation)

At duration end, between samples; duration includes ramp-up

${__P(...)} in every field

Open Model Thread Group (core, 5.5, experimental)

Open

Schedule of rate, random_arrivals, pause

Any ramp, step or hold, written as text

One new thread per arrival; unbounded

Threads interrupted when the schedule ends; add a trailing pause(...)

${__P(...)} or ${__groovy(...)} in the schedule, evaluated once

Concurrency Thread Group

Closed

Target concurrency, ramp-up, steps, hold

Linear or stepped ramp, hold

On demand, maintained at the target

Graceful, at the end of each iteration; no ramp-down

${__P(...)} in every field, re-read during the run

Ultimate Thread Group

Closed

Rows of threads, delay, startup, hold, shutdown

Spikes, waves, any sum of trapezoids

All created at start

Per row, at end time, between samples

threads_schedule property (applies to all Ultimate groups)

Stepping Thread Group (legacy)

Closed

Total, burst, step size and period, hold, stop step

Staircase up and down

All created at start

Stepwise, between samples

Field values only

Arrivals Thread Group

Open, with a cap

Arrival rate, ramp-up, steps, hold, concurrency limit

Linear or stepped rate ramp, hold

On demand, pooled; capped by the limit

Graceful

${__P(...)} in every field

Free-Form Arrivals Thread Group

Open, with a cap

Segments of start rate, end rate, duration

Any piecewise-linear rate

On demand, pooled; capped

Graceful

Field values in the table

Throughput Shaping Timer (on any closed group)

Closed, paced to an RPS schedule

Segments of start RPS, end RPS, duration

Any piecewise-linear RPS

Those of the thread group (feedback with Concurrency Thread Group)

Stops the threads when the schedule ends

load_profile or <name>.load_profile property

In practice:

  • A fixed population, as in the section’s standard load test: the core Thread Group. Use the Concurrency Thread Group instead when you want the hold separate from the ramp-up, a staircase, or graceful endings.

  • Stress to saturation: the Concurrency Thread Group with Ramp-Up Steps Count, or, for an open model, an Arrivals Thread Group with steps or a Throughput Shaping Timer with step(...).

  • Spikes and waves of users: the Ultimate Thread Group. Simultaneous bursts on one request: the Synchronizing Timer.

  • A target arrival rate with core JMeter only: the Open Model Thread Group (random arrivals, experimental). With plugins: the Arrivals or Free-Form Arrivals Thread Group when you want a concurrency cap and thread reuse.

  • A target number of requests per second across a mixed plan: the Throughput Shaping Timer on a Concurrency Thread Group with __tstFeedback.

  • Never combine two pacing mechanisms on the same scope (for example a Throughput Shaping Timer under an Arrivals Thread Group, or with a Constant Throughput Timer) unless you can explain which one limits; their delays add up.

Pitfalls

  • Minutes by default. The bzm groups pre-select minutes: a Hold of 600 meant as ten minutes is ten hours, and an Arrivals Target Rate of 4 is 4 per minute. Check the unit and the preview chart.

  • Duration semantics differ. Core Thread Group Duration includes the ramp-up; Concurrency and Arrivals Hold comes after it. Convert carefully when moving a profile between them.

  • threads_schedule and load_profile are global. One property overrides every Ultimate Thread Group, and load_profile every Throughput Shaping Timer without a named property. Use <name>.load_profile for each timer.

  • Silent parse errors. A malformed spawn(...) or step(...) is skipped with a log warning; the run continues with a different profile than intended. Read jmeter.log and compare the preview with the property.

  • Arrivals are dropped, not queued, at the concurrency limit, and they are evenly spaced. Check the Done N arrivals line against the expected count, and remember that the Open Model Thread Group produces burstier traffic.

  • A Throughput Shaping Timer is still a closed model. During a server stall its threads wait like any others, so coordinated omission applies (see the open model page). Feedback helps only up to its maximum concurrency.

  • The timer ends the test. When the RPS schedule ends, the threads in its scope are stopped; a longer thread-group hold does not extend the run.

  • Distributed runs multiply. Every remote server runs the whole plan: three generators with const(20,10m) send 60 RPS, and three with an Ultimate row of 150 threads add 450. Divide the numbers per server (with -G properties), as explained in the distributed testing page of this section.

  • Plugins must exist everywhere. A plan saved with these elements fails to load on a JMeter without the plugin; install them on every generator and in every CI image (install-for-jmx).

From the books to 5.6 to 6.0

Era State of custom thread groups and load shaping

JMeter 2.11 (JMeter Cookbook, ch. 6)

The Ultimate and Stepping Thread Groups and the Throughput Shaping Timer come from the jmeter-plugins "Standard" set, installed by unzipping archives into JMeter’s folders. Core JMeter only has the Thread Group and the Constant Throughput Timer for rate control.

Between the books and 5.x

The Plugins Manager replaces manual installation. The Concurrency, Arrivals and Free-Form Arrivals Thread Groups (contributed by BlazeMeter) join Ultimate and Stepping in jpgc-casutg, and the wiki recommends the Concurrency Thread Group as the replacement for Stepping. Core JMeter 4.0 adds the Precise Throughput Timer.

JMeter 5.2 to 5.6.3

Core adds Same user on each iteration (5.2) and the Open Model Thread Group (5.5). jpgc-casutg follows: 3.0 brings Same User On Each Iteration to the bzm groups, 3.1 and 3.1.1 support the iteration counter introduced in JMeter 5.2. jpgc-tst 2.5 fixes negative values from __tstFeedback and 2.6 adds named load_profile properties.

JMeter 6.0 (planned)

Planned per xdocs/changes.xml on master: Java 17 or later, and delays of the Open Model Thread Group and Precise Throughput Timer measured from the Thread Group start. Nothing in the planned changes concerns these plugins, and their compatibility with 6.0 has not been announced; verify after upgrading.

The Packt cookbook’s approach of choosing a profile shape for the question being asked (steps for capacity, spikes for resilience, a fixed rate for throughput objectives) is still the right one; only the installation, the extra groups and the core alternatives have changed.

References