Skip to content

Survey of existing DSLs

Before proposing anything, the question worth answering is whether one of these already does it. Each is assessed on one axis: how far can the workload model be expressed separately from the execution mechanics?

Gatling — the most expressive scenario DSL

Section titled “Gatling — the most expressive scenario DSL”

Gatling’s Scala/Java/Kotlin DSL is the closest thing to a workload language in mainstream use, and its injection profiles are genuinely first-class:

setUp(
browse.injectOpen(rampUsers(200).during(5.minutes)),
checkout.injectOpen(constantUsersPerSec(5).during(30.minutes))
).protocols(httpProtocol)
.assertions(global.responseTime.percentile(95).lt(300))

That block is a workload model, and it reads as one.

Where it stops. The scenarios it composes are defined in the same file, in the same language, interleaved with request construction, correlation and checks. The setUp block is separable in principle and nothing enforces it; in every real script it sits at the bottom of several hundred lines it depends on. Traffic mix is expressed as parallel injection profiles rather than as proportions, which is a different mental model from how anyone describes traffic.

k6 — declarative thresholds, imperative everything else

Section titled “k6 — declarative thresholds, imperative everything else”

k6 separates two things well, and they are the two most valuable:

export const options = {
scenarios: {
browse: { executor: 'ramping-vus', stages: [...] },
checkout: { executor: 'constant-arrival-rate', rate: 5, timeUnit: '1s' },
},
thresholds: { http_req_duration: ['p(95)<300'], http_req_failed: ['rate<0.001'] },
};

The options object is declarative, and it is data — it could be generated, diffed and reviewed on its own. thresholds is the SLO expressed where the run enforces it, which is the single best idea in any of these tools.

Where it stops. Everything below options is imperative JavaScript. The journey — what a browsing user actually does — is a function, and the proportions between journeys are expressed indirectly through executor rates rather than as a mix. Think time is a sleep() call inside the function, so it is invisible from the model.

Notable: k6’s arrival-rate executors are the right abstraction. Users arriving per second models traffic; concurrent virtual users models a consequence of traffic, and confusing the two is one of the most common performance-testing errors.

Locust — Python, with a mix that is actually a mix

Section titled “Locust — Python, with a mix that is actually a mix”
class BrowsingUser(HttpUser):
wait_time = between(3, 8)
weight = 7
@task(3)
def view_product(self): ...

Locust expresses weights natively — both between user classes and between tasks. That is closer to how traffic is described in conversation than anything else here, and wait_time makes think time a declared property of the user rather than a statement in the body.

Where it stops. It is still Python classes with the requests inline, and the thresholds are not part of the model at all — pass/fail is asserted after the run by external code.

Artillery — declarative, and the closest in shape

Section titled “Artillery — declarative, and the closest in shape”
config:
phases:
- duration: 300, arrivalRate: 10, rampTo: 50
ensure:
p95: 300
scenarios:
- weight: 70
flow: [ { get: { url: "/products" } }, { think: 5 } ]

YAML, phases, weights, think time and thresholds all declarative. On the axis this survey cares about, Artillery is the closest existing answer.

Where it stops. The flow is a list of HTTP operations, so the moment something needs a computed value, a conditional, or correlation beyond capturing a variable, it drops into inline JavaScript — and once one scenario does that, the file is no longer a model. Ecosystem and reporting are also thinner than the others’.

.jmx is generated XML describing a tree of components. It contains the model and the mechanics, and it is not human-authorable at all: the GUI is the authoring tool, so the artefact cannot be reviewed in a pull request or merged sensibly.

Worth including because it is what many organisations still run, and because it shows the failure mode at its extreme: a model that exists only inside a file nobody can read is a model nobody checks.

JMeter DSL (the Java library) fixes the authorability and not the separation.

Tool Model declarative? Mix as proportions? Think time declared? Thresholds in the model?
Gatling Partly (setUp) No — parallel profiles No Yes (assertions)
k6 Partly (options) Indirect No Yes
Locust No Yes (weights) Yes No
Artillery Yes Yes Yes Yes (ensure)
JMeter No Via controllers Via timers Via assertions

Nobody has all four, and the pieces all exist. Artillery is closest and gives up expressiveness to get there; k6 has the best threshold model; Locust has the best mix and think-time model; Gatling has the best injection profiles.

The gap is not capability — it is that every tool couples the model to its own execution. A workload model in any of these is a workload model for that runner, which is why a change of tool is a rewrite and why the model cannot be generated from anything.

And the honest counter-reading: Artillery being this close suggests the remaining gap might be worth a convention rather than a language — outcome 2 in the section overview. Any proposal has to argue against “just use Artillery, and forbid inline JavaScript”, which is free.

That is what the sketch has to answer.