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:
<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:
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:
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 anidfield, 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.
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 withmqtt.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
Publishexceeds 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:
| Profile | Use it to test | Example |
|---|---|---|
atOnceUsers(n) | A reconnect storm: every device connects at the same moment | atOnceUsers(5) |
rampUsers(n).during(d) | A steady fleet coming online; the usual starting point | rampUsers(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 down | rampUsersPerSec(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
export MQTT_USER=username MQTT_PASS=password
./mvnw gatling:test \
-Dgatling.simulationClass=mqtt.MqttTelemetrySimulation \
-Dusers=5With 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:
| Request | Response time means | What to look for |
|---|---|---|
| Connect | Until the broker accepts the connection (CONNACK), TLS included | Failures or slow connects as users ramp up |
| Subscribe | Until the broker confirms the subscription (SUBACK) | Rarely a bottleneck; failures usually mean ACL problems |
Publish (with await) | From publish until the correlated message arrives | End-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
awaitmean 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
- Model sessions, not requests. Real devices stay connected; keep the connection open across the loop instead of connecting per message.
- Match payload sizes. Replace the tiny JSON with payloads shaped like yours; check their wire size with the packet size calculator.
- Separate fan-out from one-to-one. A dashboard scenario that subscribes to
testmqtt/gatling/+/datareceives every device's messages; run it as its own scenario so its load is visible. - Watch the load generator. A saturated JVM inflates response times. Check CPU and GC on the machine running Gatling.
- 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.



