Testing distributed systems is fundamentally different from testing monoliths because the failures aren’t isolated.

Let’s see how JMeter handles it when your microservices are spread across multiple machines.

Imagine you have three services: AuthService on node1, UserService on node2, and OrderService on node3. You want to simulate 1000 concurrent users hitting AuthService, which then calls UserService and OrderService internally.

Here’s a simplified JMeter test plan (test.jmx):

<hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Microservices Test" enabled="true">
        <stringProp name="TestPlan.comments"></stringProp>
        <boolProp name="TestPlan.functional_mode">false</boolProp>
        <boolProp name="TestPlan.serialize_thread_groups">false</boolProp>
        <elementProp name="TestPlan.threadGroups" elementType="collection"/>
    </TestPlan>
    <hashTree>
        <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Auth Thread Group" enabled="true">
            <stringProp name="ThreadGroup.num_threads">1000</stringProp>
            <stringProp name="ThreadGroup.ramp_time">60</stringProp>
            <boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
            <stringProp name="ThreadGroup.duration"></stringProp>
            <stringProp name="ThreadGroup.delay"></stringProp>
            <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" enabled="true">
                <boolProp name="LoopController.continue_forever">true</boolProp>
                <stringProp name="LoopController.loops">-1</stringProp>
            </elementProp>
        </ThreadGroup>
        <hashTree>
            <HTTPSamplerProxy guiclass="HttpSamplerGui" testclass="HTTPSamplerProxy" testname="Login Request" enabled="true">
                <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
                    <collectionProp name="Arguments.arguments">
                        <elementProp name="" elementType="HTTPArgument">
                            <boolProp name="HTTPArgument.always_encode">false</boolProp>
                            <stringProp name="Argument.name">username</stringProp>
                            <stringProp name="Argument.value">testuser</stringProp>
                            <boolProp name="HTTPArgument.use_equals">true</boolProp>
                            <stringProp name="Argument.path"></stringProp>
                        </elementProp>
                        <elementProp name="" elementType="HTTPArgument">
                            <boolProp name="HTTPArgument.always_encode">false</boolProp>
                            <stringProp name="Argument.name">password</stringProp>
                            <stringProp name="Argument.value">password123</stringProp>
                            <boolProp name="HTTPArgument.use_equals">true</boolProp>
                            <stringProp name="Argument.path"></stringProp>
                        </elementProp>
                    </collectionProp>
                </elementProp>
                <stringProp name="HTTPSampler.domain">node1</stringProp>
                <stringProp name="HTTPSampler.port">8080</stringProp>
                <stringProp name="HTTPSampler.protocol">http</stringProp>
                <stringProp name="HTTPSampler.contentEncoding"></stringProp>
                <stringProp name="HTTPSampler.path">/auth/login</stringProp>
                <stringProp name="HTTPSampler.method">POST</stringProp>
                <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
                <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
                <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
                <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
                <stringProp name="HTTPSampler.connect_timeout"></stringProp>
                <stringProp name="HTTPSampler.response_timeout"></stringProp>
            </HTTPSamplerProxy>
            <hashTree/>
            <HTTPSamplerProxy guiclass="HttpSamplerGui" testclass="HTTPSamplerProxy" testname="Get User Info" enabled="true">
                <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" enabled="true">
                    <collectionProp name="Arguments.arguments"/>
                </elementProp>
                <stringProp name="HTTPSampler.domain">node1</stringProp>
                <stringProp name="HTTPSampler.port">8080</stringProp>
                <stringProp name="HTTPSampler.protocol">http</stringProp>
                <stringProp name="HTTPSampler.contentEncoding"></stringProp>
                <stringProp name="HTTPSampler.path">/auth/userinfo</stringProp>
                <stringProp name="HTTPSampler.method">GET</stringProp>
                <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
                <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
                <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
                <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
                <stringProp name="HTTPSampler.connect_timeout"></stringProp>
                <stringProp name="HTTPSampler.response_timeout"></stringProp>
            </HTTPSamplerProxy>
            <hashTree/>
        </hashTree>
    </hashTree>
</hashTree>

This plan targets node1:8080 for /auth/login and /auth/userinfo. JMeter, by default, runs this test from a single machine. However, for microservices, you need to distribute the load generation across multiple machines to avoid your load generator becoming the bottleneck.

To achieve distributed testing, you need to configure JMeter in client-server mode. This involves one or more "master" (controller) machines and multiple "agent" (remote) machines.

Here’s how you set it up:

  1. Install JMeter on all machines: Ensure JMeter is installed on your master and all agent machines.
  2. Configure jmeter.properties on the Master:
    • Locate the jmeter.properties file in your JMeter installation directory.
    • Find the remote_hosts property and uncomment it.
    • List the IP addresses or hostnames of your agent machines, separated by commas. For example:
      remote_hosts=192.168.1.101,192.168.1.102,192.168.1.103
      
  3. Configure jmeter.properties on the Agents:
    • On each agent machine, find jmeter.properties.
    • Set server.rmi.localport to a specific port (e.g., server.rmi.localport=1099). This is the port JMeter’s RMI server will listen on.
    • Ensure server.rmi.ssl.disable=true is set if you’re not using SSL for RMI communication (common in test environments).
  4. Start JMeter Agents:
    • On each agent machine, navigate to your JMeter bin directory.
    • Run the command: jmeter-server
    • This starts the RMI server, making the agent ready to receive commands from the master.
  5. Start JMeter Master:
    • On the master machine, navigate to your JMeter bin directory.
    • Run JMeter from the command line, specifying the test plan and the -r (remote) flag:
      jmeter -n -t test.jmx -r
      
      Alternatively, if you want to specify the remote hosts explicitly on the command line (overriding remote_hosts in jmeter.properties):
      jmeter -n -t test.jmx -R 192.168.1.101,192.168.1.102,192.168.1.103
      

When you run this, the master JMeter orchestrates the test. It distributes the test plan to the agents, tells them to start executing the load, and collects the results back from them. Each agent runs a portion of the total threads. If you specified 1000 threads in your ThreadGroup and have 3 agents, each agent will likely run approximately 333 threads (JMeter distributes as evenly as possible).

The key benefit here is that your load generation is no longer limited by the CPU and memory of a single machine. You can scale your load testing infrastructure by adding more agent machines.

The trickiest part of distributed testing is often network configuration. Ensure that the master can reach each agent on the RMI port (default 1099, or whatever you set server.rmi.localport to) and that the agents can reach the services being tested (e.g., node1:8080). Firewalls are the usual culprits.

After successfully running your distributed test, the next common hurdle is correlating results from multiple agents to understand the overall performance of your distributed services.

Want structured learning?

Take the full Jmeter course →