Broker Testing

MQTT Load Testing with Gatling: Setup, Scenario and Reports

Load test an MQTT broker with Gatling: add the MQTT plugin, write a Java simulation with connect, subscribe and publish checks, then read the HTML report.

Updated · 9 min read

MQTT Load Testing with Gatling: Setup, Scenario and Reports

Gatling can load test an MQTT broker with its MQTT plugin: you describe device behaviour as a scenario in Java, Kotlin or Scala — connect, subscribe, publish, wait for the matching message — and Gatling injects virtual users and produces its HTML report with response-time percentiles and throughput. It suits teams that already use Gatling for HTTP and want the same simulation-as-code workflow for MQTT.

Before you start, know the licensing: the MQTT component is distributed under the Gatling Enterprise Component License. Locally with the Community Edition it is limited to 5 virtual users and 5-minute tests, enough to develop and validate a simulation. Running it at real scale requires Gatling Enterprise. If that does not fit, k6 with xk6-mqtt, JMeter or emqtt-bench are open-source alternatives.

Project setup

The easiest start is one of Gatling's Maven or Gradle demo projects. Then add the MQTT plugin next to the standard Gatling dependencies:

pom.xml (excerpt)
<dependency>
  <groupId>io.gatling</groupId>
  <artifactId>gatling-mqtt-java</artifactId>
  <version>3.16.0</version>
  <scope>test</scope>
</dependency>

Use the same version as your other Gatling artifacts. Scala projects use gatling-mqtt instead of gatling-mqtt-java; Gradle projects declare it as gatlingImplementation("io.gatling:gatling-mqtt-java:3.16.0").

The MQTT DSL is not imported by default, so every simulation needs:

Imports
import io.gatling.javaapi.mqtt.*;
import static io.gatling.javaapi.mqtt.MqttDsl.*;

Before writing the simulation, confirm the broker, port and credentials with a single client in the online MQTT client. Then you know any later failure comes from load, not configuration. Use the shared broker.connectmq.com endpoint only to validate the scenario; for real load, set .broker(...) and the credentials to your own private broker instance.

The protocol: broker, credentials, QoS

The protocol builder holds settings shared by all virtual users:

Protocol configuration
MqttProtocolBuilder mqttProtocol = mqtt
    .mqttVersion_5()                         // or mqttVersion_3_1_1() (default)
    .broker("broker.connectmq.com", 1883)    // TLS: port 8883 + .useTls(true)
    .credentials(System.getenv("MQTT_USER"), System.getenv("MQTT_PASS"))
    .keepAlive(30)
    .qosAtLeastOnce()                        // default is QoS 0
    .cleanSession(true)
    .reconnectAttemptsMax(3)
    .correlateBy(jsonPath("$.id"));

Two settings deserve attention:

  • Client ID. If you do not set clientId(...), Gatling generates a random one per virtual user — the safe default for load tests. If you set it from a feeder, make sure values are unique, or users will take over each other's sessions.
  • correlateBy. Tells Gatling how to match an inbound message to the request that is waiting for it. Here every payload carries an id field, so the message a user receives can be matched to the publish it sent.

A device telemetry scenario

The scenario below models a device: it connects, subscribes to its own topic, then publishes a reading every second for a minute and waits for each one to come back through the broker.

src/test/java/mqtt/MqttTelemetrySimulation.java
package mqtt;

import static io.gatling.javaapi.core.CoreDsl.*;
import static io.gatling.javaapi.mqtt.MqttDsl.*;

import io.gatling.javaapi.core.*;
import io.gatling.javaapi.mqtt.*;
import java.time.Duration;
import java.util.Iterator;
import java.util.Map;
import java.util.stream.Stream;

public class MqttTelemetrySimulation extends Simulation {

  MqttProtocolBuilder mqttProtocol = mqtt
      .mqttVersion_5()
      .broker("broker.connectmq.com", 1883)
      .credentials(System.getenv("MQTT_USER"), System.getenv("MQTT_PASS"))
      .keepAlive(30)
      .qosAtLeastOnce()
      .correlateBy(jsonPath("$.id"));

  Iterator<Map<String, Object>> devices =
      Stream.iterate(1, i -> i + 1)
          .map(i -> Map.<String, Object>of("deviceId", "dev-" + i))
          .iterator();

  ScenarioBuilder telemetry = scenario("Device telemetry")
      .feed(devices)
      .exec(mqtt("Connect").connect())
      .exec(mqtt("Subscribe").subscribe("testmqtt/gatling/#{deviceId}/data"))
      .repeat(60, "n").on(
          exec(
              mqtt("Publish")
                  .publish("testmqtt/gatling/#{deviceId}/data")
                  .message(StringBody("{\"id\":\"#{deviceId}-#{n}\",\"temp\":21.5}"))
                  .await(Duration.ofSeconds(2))
                  .check(jsonPath("$.temp").exists())
          ).pause(Duration.ofSeconds(1))
      );

  {
    int users = Integer.getInteger("users", 5);
    setUp(
        telemetry.injectOpen(rampUsers(users).during(Duration.ofSeconds(30)))
    ).protocols(mqttProtocol)
     .assertions(
         global().failedRequests().percent().lt(1.0),
         details("Publish").responseTime().percentile3().lt(200)
     );
  }
}

What each piece does:

  • Feeder. Gives each virtual user a unique deviceId, used in the topic and in the message id.
  • repeat(60, "n"). Loops 60 times and exposes the counter as #{n}, so every message id is unique — required for correlation.
  • .await(Duration.ofSeconds(2)). A blocking check: the user waits until a message matching the correlation id arrives on a subscribed topic, or fails after 2 seconds. .expect(...) is the non-blocking variant; combine it with mqtt.waitForMessages() when you want to publish several messages before waiting.
  • .check(jsonPath("$.temp").exists()). Validates the content of the matched message, not just its arrival.
  • Assertions. Fail the run if more than 1 % of requests fail or the 95th percentile of Publish exceeds 200 ms (percentile3() is the 95th by default). Adjust both to your own baseline.

Topics follow testmqtt/gatling/<device>/data; if you later add wildcard subscriptions, verify the patterns in the topic matcher.

Choosing an injection profile

In an MQTT simulation each virtual user is a device that holds a connection for the whole scenario, so the injection profile decides how many connections exist at once and how fast they arrive:

ProfileUse it to testExample
atOnceUsers(n)A reconnect storm: every device connects at the same momentatOnceUsers(5)
rampUsers(n).during(d)A steady fleet coming online; the usual starting pointrampUsers(5).during(Duration.ofSeconds(30))
constantUsersPerSec(r).during(d)Continuous arrival of new devices (sessions that start and end)constantUsersPerSec(1).during(Duration.ofMinutes(2))
rampUsersPerSec(a).to(b).during(d)A growing arrival rate, to find the point where connects slow downrampUsersPerSec(1).to(5).during(Duration.ofMinutes(1))

Concurrent connections are roughly arrival rate × scenario duration. With 60 one-second publishes per user, each user stays connected for about a minute, so constantUsersPerSec(10) settles at around 600 concurrent devices. Keep this arithmetic in mind when you size a run, and remember that with the Community Edition the total stays capped at 5 users.

You can also combine scenarios in one setUp, for example a ramp of telemetry devices plus a small group of dashboard users subscribing to a wildcard. Each scenario keeps its own request names, so the report shows both loads separately.

Run the simulation

Run with Maven
export MQTT_USER=username MQTT_PASS=password
./mvnw gatling:test \
  -Dgatling.simulationClass=mqtt.MqttTelemetrySimulation \
  -Dusers=5

With the Community Edition, keep users at 5 or below and the duration under 5 minutes — the run stops at those limits. That is enough to prove the scenario, correlation and checks work. Upload the same project to Gatling Enterprise to run it with thousands of users and multiple load generators.

Reading the HTML report

At the end Gatling prints the path to target/gatling/<simulation>-<timestamp>/index.html. The report groups everything by request name, so the names in mqtt("…") are your headings:

RequestResponse time meansWhat to look for
ConnectUntil the broker accepts the connection (CONNACK), TLS includedFailures or slow connects as users ramp up
SubscribeUntil the broker confirms the subscription (SUBACK)Rarely a bottleneck; failures usually mean ACL problems
Publish (with await)From publish until the correlated message arrivesEnd-to-end latency percentiles and timeouts

Useful charts:

  • Response time percentiles over time. A rising p95 or p99 while users ramp up shows where the broker or network starts to saturate.
  • Requests per second. Compare it with the rate you intended (users × 1 msg/s here). If it plateaus below target, something is throttling.
  • Errors table. Timeouts on await mean messages did not come back in time — check QoS, queue limits and why messages are lost. Connection errors at a round number of users often point to a limit; decode broker reason codes with the MQTT reason code tool.

A broader method for separating broker limits from load-generator limits is in interpreting MQTT benchmark results.

Tips for realistic simulations

  1. Model sessions, not requests. Real devices stay connected; keep the connection open across the loop instead of connecting per message.
  2. Match payload sizes. Replace the tiny JSON with payloads shaped like yours; check their wire size with the packet size calculator.
  3. Separate fan-out from one-to-one. A dashboard scenario that subscribes to testmqtt/gatling/+/data receives every device's messages; run it as its own scenario so its load is visible.
  4. Watch the load generator. A saturated JVM inflates response times. Check CPU and GC on the machine running Gatling.
  5. Commit the simulation. Re-run it after broker upgrades and configuration changes to catch regressions.

Watch Gatling traffic in the online client

Gatling's MQTT plugin gives you readable, version-controlled scenarios with correlation and checks that most MQTT tools lack. Build and validate the simulation within the Community Edition limits, then run it at scale where your licensing allows — and always read publish times with await checks, because that is the latency your devices feel.

Frequently asked questions

Is Gatling's MQTT support free?

The MQTT plugin (gatling-mqtt-java) is published on Maven Central under the Gatling Enterprise Component License. With the Community Edition it is limited to 5 virtual users and 5-minute tests; the limits are lifted when the simulation runs on Gatling Enterprise.

Does Gatling support MQTT 5?

Yes, the protocol builder offers mqttVersion_3_1(), mqttVersion_3_1_1() (the default) and mqttVersion_5(). Gatling notes that not every MQTT 5 feature may be available yet, so check the docs if you depend on a specific one.

How does Gatling measure MQTT response time?

Connect and subscribe are timed until the broker answers. For publish, add an await or expect check: Gatling then records the time until a matching inbound message arrives, which gives you end-to-end latency in the report.