skip to content

Scaling Limits

What adding engines actually costs: the plan is never split so thread counts multiply, data files do not travel with it, and every sample still has to reach one controller.

on this pageshow

explore

questions

10

A JMeter Thread Group asks for 1,000 threads and the plan runs on six remote engines - how many threads hit the target?

level: middleimportance: must knowfreq 74%

answer

  1. The plan is copied, not split
  2. The controller injects nothing itself
  3. The Thread Group field is per engine
  4. One thousand, and six engines

basics

~10 s

Six thousand. JMeter never splits a plan across engines: the controller sends the whole plan to each one, so the Thread Group's 1,000 threads start once per server. You do the division, not JMeter.

solid answer

~50 s

JMeter replicates a plan; it never shards it. The controller serialises the whole test tree and hands an identical copy to every remote engine over RMI, and each engine runs that plan in full through an ordinary local engine. A Thread Group set to 1,000 threads therefore starts 1,000 threads **on each** of the six servers, and the target sees **6,000**. The controller starts no threads of its own in a remote run - it configures the engines, tells them to start, and collects the samples they send back. The division is yours to do: put the per-engine share in the `ThreadGroup.num_threads` field (167 if you want roughly 1,000 in total across six engines) and remember that every other per-engine number in the plan - loop counts, per-engine rate targets, anything scoped to one JVM - is multiplied by the engine count in the same way.

code

xml · 6 lines
xml
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group" enabled="true">
  <stringProp name="ThreadGroup.num_threads">1000</stringProp>
  <stringProp name="ThreadGroup.ramp_time">300</stringProp>
  <boolProp name="ThreadGroup.scheduler">false</boolProp>
  <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
</ThreadGroup>

go deeper

for a junior

Remember the shape: one plan, copied whole to every engine. Asked what 1,000 threads on six JMeter servers gives, the answer is 6,000 - not 167 each, and not 1,000 spread around.

for a middle

Be able to say why. The controller serialises the tree and each engine runs it in full, so every number the plan scopes to one JVM gets multiplied by the engine count.

for a senior

Show that you compute the applied total before every distributed run, and that you treat a lost or added engine as a change to the load rather than an operational detail.

for a principal

Own where the division lives. Decide whether the per-engine share is baked into the .jmx or supplied at run time, and make the run record the engine count beside the result so old runs stay comparable.

## Replication, not sharding JMeter's distributed mode copies a test plan; it does not cut it up. When a controller starts a remote run it clones the test tree, serialises it, and calls the same configure step on **every** engine in its list with the identical tree. Each `jmeter-server` process hands that tree to a plain local engine - the very same engine class a standalone run uses - and executes it from the top. Nothing in the wire protocol carries a "which slice am I" hint, and no stock element in the plan reads one. That gives the arithmetic every JMeter operator learns exactly once: a Thread Group whose **Number of Threads** field reads `1000`, started against six servers, applies **6,000** threads. The JMeter 6.0.0 manual states it in those words, and the code agrees - the controller loops over the host list creating one client engine per host and sends each the same tree. ## The controller is not a seventh generator When a run is started remotely, the controller builds *only* client-side engines. It does not additionally create a local engine for itself, so the total is `threads x engines`, never `threads x (engines + 1)`. Its own CPU goes on serialising the plan out, deserialising sample results back, and writing the result file. An overloaded controller therefore shows up as a *collection* problem, not as missing load - the servers still applied their full share. ## What multiplies and what does not | Plan setting | Scope it is evaluated in | Effect across six engines | |---|---|---| | `ThreadGroup.num_threads` | one engine | x6 threads applied | | Loop count / iteration budget | one thread on one engine | x6 total iterations | | Ramp-up period | one engine, on its own clock | unchanged; all six ramp in parallel | | A file read position | one JVM | six independent positions | | A "shared across all threads" scope | one JVM | six independent groups | The pattern is one rule: **anything JMeter scopes to a JVM becomes six of that thing.** Durations and periods are the exception, because the engines run concurrently rather than in sequence - six engines ramping for five minutes still take five minutes. ## Doing the division yourself 1. Decide the total the target must see. That decision is a workload question and lives outside JMeter. 2. Count the engines you will actually start - the number you *intend* to start is not the number that came up. 3. Put the quotient in the Thread Group field, and accept the rounding: 167 on six engines is 1,002, not 1,000. 4. Walk the rest of the plan for other per-engine numbers and divide those too. 5. Record the engine count next to the result. A number from "1,000 threads" means nothing without it. ## Where this bites in practice - **An engine is added "to be safe".** Six becomes seven and the applied load rises by a sixth, with no change to the `.jmx` and nothing in the result file that says so. - **An engine fails to come up and the run continues on the rest.** The applied load silently drops by a sixth; the plan is byte-identical to yesterday's. - **A plan is promoted from a laptop smoke run.** The field that was right for one engine is now six times too large. - **Two Thread Groups in one plan.** Both replicate, so the multiplier applies to the whole plan, not to a single group. - **A run is compared to an older one.** Unless the engine count matched, the two runs applied different loads whatever the field says. ## The check that costs nothing Before a distributed run, multiply the Thread Group field by the number of engines you are about to start and say the product out loud. If that product is not the number you meant to apply, stop and fix the field. The mistake this catches - a plan that quietly applies six times the intended concurrency and takes the target down - is the single most common distributed-JMeter incident, and it is arithmetic, not configuration. ## Why JMeter works this way The replication is not an oversight; it is the cheapest possible protocol. An engine needs no knowledge of its peers, engines can be added or lost without renegotiating anything, and a plan that runs on one machine runs unchanged on twenty. The price is that the only component that knows how many engines exist is you, standing at the controller, and JMeter never asks you to write that number down anywhere it could check.

  • You want 1,000 threads in total across six JMeter engines. What do you set, and what does the rounding cost you?
    Set `ThreadGroup.num_threads` to 167 and accept 1,002 applied threads, or drop to five engines of 200 for an exact 1,000. JMeter has no per-engine override in the plan itself, so an uneven split means either accepting the rounding or changing the engine count.
  • During a remote-started JMeter run, does the controller machine apply any load of its own?
    No. On a remote start the controller creates only client engines, one per host, and no local engine. It serialises the plan out, receives sample results, and writes the result file. Its CPU cost is collection, not injection.
  • One of your six JMeter engines fails to configure and the run proceeds on the other five. What has changed about the test?
    The applied load dropped by a sixth while the plan stayed identical. Any comparison against a six-engine baseline is now invalid. Treat the engine count as part of the run's configuration and record it with the result, not as an operational detail.

It is like emailing the same shopping list to six housemates. Nobody buys a sixth of it - you come home with six of everything. If you want the list bought once, you cut it into six lists before you send it.

saying these in an interview costs you the question

  • Says the controller splits the thread count across the servers.
  • Thinks adding an engine spreads the same load rather than adding to it.
  • Counts the controller as an extra load generator alongside the servers.
  • Assumes loop counts stay global while only threads multiply.
  • Compares two runs without checking that the engine counts matched.
open as a page

What does JMeter's mode property select for a distributed run, and what is its default?

level: middleimportance: must knowfreq 61%

basics

~10 s

JMeter's mode property names the SampleSender each engine uses to return results to the controller. It is read on the controller only, and defaults to StrippedBatch: batched sends with response bodies removed.

open as a page

A JMeter CSV Data Set Config feeds unique logins across six engines - why does every engine issue the same rows?

level: seniorimportance: must knowfreq 62%

basics

~20 s

Every engine holds its own copy of the file and its own read position, so all six start at line one and walk the same rows in step. Sharing mode scopes threads inside one JVM and never reaches across engines.

open as a page

Six JMeter engines stream samples and the controller's heap keeps filling. Which mode change helps most?

level: seniorimportance: must knowfreq 49%

basics

~20 s

Move to mode=Statistical. It is the only value that cuts the number of results reaching the controller rather than their size, replacing each label's samples with one aggregate. Stripping and batching only shrink or group what still arrives.

open as a page

In a JMeter distributed run, what does the controller actually send to each remote engine?

level: juniorimportance: should knowfreq 50%

basics

~20 s

The serialised test plan, the script name, a relative base directory, and any properties the controller was told to push. Nothing else. Data files, external script files and element classes must already exist on every server.

open as a page

After a distributed JMeter run, why does the controller's result file hold no response bodies?

level: juniorimportance: should knowfreq 52%

basics

~10 s

JMeter's mode property defaults to StrippedBatch, and every Stripped sender blanks each SampleResult's response data on the engine before it is sent. Timings, byte counts and status codes survive; the payload does not.

open as a page

In JMeter's Batch sending mode, which two properties decide when an engine flushes samples?

level: middleimportance: should knowfreq 43%

basics

~10 s

num_sample_threshold, default 100 samples, and time_threshold, default 60000 milliseconds. Whichever fires first sends the accumulated batch and resets both counters. Setting either to -1 disables that trigger.

open as a page

A JMeter plan reads a property that is set only on the controller - what do the remote engines resolve it to?

level: seniorimportance: should knowfreq 55%

basics

~20 s

Whatever that engine's own property set holds, which is usually nothing. Each engine evaluates the plan in its own JVM against properties it loaded at its own startup; the controller's property set does not travel with the plan.

open as a page

In a JMeter plan replicated to six engines, what do ${__machineName} and ${__machineIP} return on each one?

level: middleimportance: nice to knowfreq 32%

basics

~10 s

The host name and IP address of the machine that evaluates them - so each engine returns its own. They are the built-in way to tell six identical copies of one replicated plan apart.

open as a page

In JMeter's Asynch sending mode, what happens when the asynch.batch.queue.size queue fills?

level: seniorimportance: nice to knowfreq 24%

basics

~20 s

The sampler thread blocks. AsynchSampleSender first tries a non-blocking offer; when that fails it falls back to a blocking put and waits for the worker to drain space, counting the wait in QueueWaits and QueueWaitTime.

open as a page