Comparison posts usually list features. Let's do something more useful: write the same test in five tools and see how each one feels.
The test: 50 new users arrive every second for 5 minutes, each requests /products. That's an arrival rate (open model), which is how real traffic behaves: new visitors keep coming whether or not your server is keeping up. (Why that matters.)
k6 (JavaScript)
import http from 'k6/http';
export const options = {
scenarios: {
browse: {
executor: 'constant-arrival-rate',
rate: 50,
timeUnit: '1s',
duration: '5m',
preAllocatedVUs: 100,
},
},
thresholds: { http_req_duration: ['p(95)<500'] },
};
export default function () {
http.get('https://staging.example.com/products');
}
k6 run browse.js
Feels like: writing a small program. Thresholds are built in, so CI pass/fail is one line. Very efficient Go engine. Distributed runs need Kubernetes (k6-operator) or Grafana Cloud.
Locust (Python)
from locust import HttpUser, task, between
class Shopper(HttpUser):
wait_time = between(2, 5)
@task
def browse(self):
self.client.get("/products")
locust -f locustfile.py --host https://staging.example.com \
--headless -u 500 -r 50 -t 5m
Feels like: plain Python, which makes complex user behaviour easy. Note the model: Locust spawns a fixed number of users (-u 500) that loop with think time, a closed model, so the request rate drops if the server slows down. A built-in web UI shows live charts.
Artillery (YAML)
config:
target: https://staging.example.com
phases:
- duration: 300
arrivalRate: 50
scenarios:
- flow:
- get:
url: /products
npx artillery run browse.yml
Feels like: configuration, not code. Arrival rates are the default. Custom logic goes into JavaScript hooks, which gets messy for complicated flows.
Gatling (Java DSL)
import static io.gatling.javaapi.core.CoreDsl.*;
import static io.gatling.javaapi.http.HttpDsl.*;
import io.gatling.javaapi.core.*;
import io.gatling.javaapi.http.*;
public class BrowseSimulation extends Simulation {
HttpProtocolBuilder httpProtocol = http.baseUrl("https://staging.example.com");
ScenarioBuilder scn = scenario("Browse")
.exec(http("products").get("/products"));
{
setUp(scn.injectOpen(constantUsersPerSec(50).during(300)))
.protocols(httpProtocol);
}
}
Feels like: a typed DSL with great HTML reports and very high throughput per machine. Natural for JVM teams; also available in Scala, Kotlin and JavaScript.
JMeter (GUI / XML)
No snippet that fits here: a JMeter test is a .jmx XML file you usually build in the GUI (Thread Group → HTTP Request → listeners), then run headless:
jmeter -n -t browse.jmx -l results.jtl
Feels like: the veteran. Supports almost any protocol (HTTP, JDBC, JMS, FTP, LDAP) with a huge plugin ecosystem. Thread Groups are closed-model by default; arrival rates need a plugin (Concurrency/Arrivals Thread Group). Large XML test plans are hard to review in pull requests.
Side by side
| Tests in | Default model | Distributed | Best for | |
|---|---|---|---|---|
| k6 | JavaScript | Both | k6-operator / cloud | Dev teams, CI |
| Locust | Python | Closed | Built-in workers | Python teams, complex behaviour |
| Artillery | YAML + JS | Open | Cloud runners | Quick scenarios, Node teams |
| Gatling | Java/Scala/Kotlin/JS | Both | Enterprise edition | JVM teams, very high load |
| JMeter | GUI / XML | Closed | Controller + workers | Exotic protocols |
What none of them do for you
All five generate load well. What they leave to you:
- Infrastructure: running enough machines, in the right regions, for big tests.
- Realistic flows: chaining tokens, CSRF, per-user test data.
- Interpretation: a wall of percentiles doesn't say why checkout got slow.
That gap is why I'm building Hoofbeat: describe the test in plain words (or import a HAR file, OpenAPI spec or Postman collection), and it runs on cloud load generators in the regions you pick, then explains the result per step. But if your team loves tests as code, k6 or Gatling are excellent choices, and many teams use a code tool and a hosted one side by side.
The longer comparison is on the Hoofbeat blog.
Top comments (0)