JMeter’s Thread Groups are the engine room for simulating concurrent users, but the default settings can feel like a blunt instrument when you need surgical precision in controlling how many users hit your system and when.
Let’s see a basic setup in action. Imagine we want to simulate 10 users starting immediately and running for 5 minutes.
<hashTree>
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Test Plan" enabled="true">
<stringProp name="TestPlan.comments"></stringProp>
<boolProp name="TestPlan.functional_mode">false</boolProp>
<boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
<elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" enabled="true">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="TestPlan.user_define_classpath"></stringProp>
</TestPlan>
<hashTree>
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Basic 10 Users" enabled="true">
<stringProp name="ThreadGroup.on_thread_group_error">continue</stringProp>
<elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" enabled="true">
<boolProp name="LoopController.continue_forever">false</boolProp>
<stringProp name="LoopController.loops">-1</stringProp>
</elementProp>
<stringProp name="ThreadGroup.num_threads">10</stringProp>
<stringProp name="ThreadGroup.ramp_time">0</stringProp>
<boolProp name="ThreadGroup.scheduler">true</boolProp>
<stringProp name="ThreadGroup.duration">300</stringProp>
<stringProp name="ThreadGroup.delay">0</stringProp>
</ThreadGroup>
<hashTree>
<ConfigTestElement guiclass="HttpDefaultsGui" testclass="HttpDefaults" testname="HTTP Request Defaults" enabled="true">
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" enabled="true">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="HTTPSampler.domain">example.com</stringProp>
<stringProp name="HTTPSampler.port"></stringProp>
<stringProp name="HTTPSampler.protocol"></stringProp>
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
<stringProp name="HTTPSampler.path"></stringProp>
<stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
</ConfigTestElement>
<hashTree>
<HTTPSampler guiclass="HttpSamplerGui" testclass="HTTPSampler" testname="GET /" enabled="true">
<stringProp name="HTTPSampler.domain"></stringProp>
<stringProp name="HTTPSampler.port"></stringProp>
<stringProp name="HTTPSampler.protocol"></stringProp>
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
<stringProp name="HTTPSampler.path">/</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.image_parser">true</boolProp>
<boolProp name="HTTPSampler.concurrentDownload">false</boolProp>
<stringProp name="HTTPSampler.connect_timeout"></stringProp>
<stringProp name="HTTPSampler.response_timeout"></stringProp>
</HTTPSampler>
<hashTree/>
</hashTree>
</hashTree>
</hashTree>
</hashTree>
This setup runs 10 threads, meaning 10 simulated users. ramp_time is 0, so all 10 users start at the exact same instant. scheduler is true, and duration is set to 300 seconds (5 minutes). This means JMeter will keep these 10 threads alive and active for 5 minutes, after which they will all stop. The LoopController is set to -1 for loops, which combined with the scheduler, means threads will run for the specified duration and then stop, rather than looping indefinitely or a fixed number of times within that duration.
The problem this solves is simulating realistic user load. You don’t want all your users to magically appear at t=0. You want to model how users actually join a system – gradually, or in bursts.
Internally, the ThreadGroup is responsible for creating and managing these virtual users (threads). When you configure num_threads, ramp_time, and duration/loops, you’re telling JMeter how to orchestrate the lifecycle of these threads. The ramp_time is crucial: it’s the total time over which JMeter will spin up all the num_threads. So, with num_threads=10 and ramp_time=10, JMeter will start one new thread every second for 10 seconds, until all 10 threads are active.
The duration parameter, when scheduler is enabled, dictates the total runtime of the thread group. Threads will be kept alive and executing their samplers for this duration. If scheduler is false, the LoopController takes over, and threads will run for the number of loops specified.
One of the most powerful, yet often overlooked, aspects of Thread Groups is the Ultimate Thread Group plugin. It allows for highly complex ramp-up and ramp-down scenarios that the standard Thread Group cannot express. You can define multiple "startup" steps, each with its own starting threads, ramp-up time, and hold load time. For instance, you could spin up 50 users over 60 seconds, hold that load for 5 minutes, then ramp down 20 users over 30 seconds, and so on. This is essential for modeling scenarios like gradual user growth followed by a peak, or a sudden surge and then a gradual decline.
The next concept you’ll grapple with is how to make those simulated users do something interesting beyond just hitting the root of a server – that’s where Samplers, Controllers, and Listeners come into play.