Next-Gen App & Browser Testing Cloud
Trusted by 2 Mn+ QAs & Devs to accelerate their release cycles

Run load tests across machines and regions without a generator fleet. Verified JMeter and Gatling walkthroughs, copy-paste config, and real job output data.

Anmol Gupta
Author

Japneet Singh Chawla
Reviewer
Published on: August 31, 2026
Key takeaways
Running a performance test at scale means generating load from several machines at once, which changes what your stated user count actually produces. HyperExecute provisions those machines on demand and exposes the regional split as a configuration field, so JMeter and Gatling plans run across regions with no load-generator fleet to maintain.
What Changes When Load Is Distributed
Which Path Should You Start With?
A checkout endpoint that responds instantly in staging can still collapse the Monday after release, when real concurrent traffic arrives. The functional suite never saw it coming, because every test in that suite exercised the endpoint one user at a time.
Web performance work has published, measured outcomes attached to it. In a case study published in March 2021, Vodafone ran an A/B test where version A was optimized for Web Vitals and scored 31% better on Largest Contentful Paint in the field than version B, and the published results report that the faster version generated 8% more sales, an 11% improvement in cart-to-visit rate, and a 15% improvement in lead-to-visit rate.
Running one load test from one machine is straightforward. Running enough load to represent real traffic means generating it from several machines at once, and that is where the numbers stop meaning what you think they mean.
The trap below is not specific to any one platform. It applies to any setup that spreads load across more than one generator, and it is the single most common reason a capacity number turns out to be wrong.
The mechanism is simple. A thread count written into a .jmx file is a per-generator instruction, not a global one, so every machine you add runs the whole plan again.
Verify the real number before trusting any result. Compare the sample count in the results file against what your plan should have produced, which for the run above was 30 samples per task from 10 threads looping 3 times.
Load can originate from six documented regions, and the default is East US in Richmond, Virginia. A test that never touches this setting measures latency from Virginia regardless of where your customers actually are.
The 2000-user figure quoted for JMeter is a ceiling the infrastructure can support under favorable conditions, meaning lightweight requests, sensible timeouts, and load spread across enough machines and regions. Treat it as a planning bound rather than a target.
The rest of this guide is a worked solution on HyperExecute, the TestMu AI orchestration cloud, because it provisions the generators for you and exposes the regional split as a configuration field rather than something you wire up yourself. Both routes are covered: uploading a plan through the portal, and driving the same run from a config file in CI.
Everything that follows was validated against HyperExecute CLI v0.2.351, and the console output and response-time figures come from a job that actually ran. If you want conceptual grounding on test types first, the performance testing guide covers load, stress, spike, and soak testing as categories.
For the upload path, a TestMu AI account and a test file are the whole list. There is no load-generator fleet to provision, because HyperExecute allocates machines per task and releases them when the job ends.
# Linux
curl -O https://downloads.lambdatest.com/hyperexecute/linux/hyperexecute
chmod +x hyperexecute
# macOS
curl -O https://downloads.lambdatest.com/hyperexecute/darwin/hyperexecute
chmod u+x ./hyperexecute
# Windows (PowerShell)
Invoke-WebRequest -Uri "https://downloads.lambdatest.com/hyperexecute/windows/hyperexecute.exe" -OutFile "hyperexecute.exe"
# Credentials, read automatically by the CLI
export LT_USERNAME="your_username"
export LT_ACCESS_KEY="your_access_key"Confirm the binary before writing config, because schema rules changed between YAML versions and error messages reference the version you are on.
$ ./hyperexecute --version
HyperExecute version 0.2.351JMeter is the tool the TestMu AI documentation covers most thoroughly, and the portal flow is the officially documented route. The CLI route also works and is covered second, since the documentation does not describe it.
This path needs no config file and no CLI. Author the plan in the Apache JMeter GUI as you normally would, then hand the file to HyperExecute and configure the load in a form.
Step 1: Create the project and upload the plan
Step 2: Configure the load
Each field changes what your results actually mean, so it is worth knowing what you are setting rather than accepting defaults.
| Field | What it controls | Practical note |
|---|---|---|
| Total Users | The number of users you intend to test for | Read the load math section before trusting this figure, because it interacts with machine count |
| Duration (min) | How long the test runs | Pair it with ramp-up rather than setting it alone |
| Ramp-up Time (min) | The amount of time it should take to reach the peak test load | A realistic ramp separates a real capacity limit from a cold-start artifact |
| Total Load Distribution | The regions load is generated from, as a percentage of users per region | Defaults to East US, so untouched tests measure latency from Virginia |
| Machine count | The number of machines used for parallel test execution | This is the multiplier in the load math trap |
| Split CSV | Splits input data from a CSV file among different threads or regions | Without it every generator replays identical test data |
| Java Version | The runtime the plan executes on | Java 11 is the default and Java 25 is available |
| Job Labels | Tags applied to the execution for identification | Makes performance runs filterable alongside functional builds |
Two JMeter user properties can be overridden from the same screen, and both are genuinely useful rather than obscure.
Step 3: Read the results
Open the Jobs section, where a completed JMeter run exposes five views. The JMeter on HyperExecute documentation covers each screen in detail.
Summary opens on six tiles: virtual users, average response time, average throughput, error percentage, 90th percentile response time, and average bandwidth. Underneath, the Load and Response Time charts share a time axis, and Additional Details lists start time, end time, and every region the load came from.

Reading the two charts together is the whole skill. When the users line keeps climbing while response time climbs with it, you have found the saturation point rather than a flat capacity wall.
Timeline Report plots performance across the run, which is where a gradual degradation curve separates itself from a single spike.

Request Stats breaks the run down per request, so a single slow endpoint stops hiding inside a healthy-looking average.

Errors lists error codes with counts and percentages. Check this before celebrating a fast run, because a high error rate makes throughput look better than it is.

Logs holds the raw execution output, which is where you go when the other four screens agree that something failed but not why.

The portal flow needs a human clicking a form, which rules it out of a pipeline. HyperExecute orchestrates any tool with a command-line runner, so JMeter can be driven from YAML even though the documentation does not describe that route.
The config below installs JMeter, discovers every plan in a directory, and runs each one on its own machine. It was validated and executed on CLI v0.2.351.
hyperexecute.yaml
---
version: 0.1
runson: linux
autosplit: true
concurrency: 2
runtime:
- language: java
version: "17"
pre:
- wget -q https://archive.apache.org/dist/jmeter/binaries/apache-jmeter-5.6.3.tgz
- tar -xzf apache-jmeter-5.6.3.tgz
- mkdir -p results
testDiscovery:
type: raw
mode: static
command: ls plans/*.jmx
testRunnerCommand: ./apache-jmeter-5.6.3/bin/jmeter -n -t $test -l results/result.jtl -j results/jmeter.log
post:
- tail -5 results/result.jtl
scenarioCommandStatusOnly: true
report: true
partialReports:
location: results
type: html
uploadArtefacts:
- name: jmeter-results
path:
- results/**
jobLabel: [Performance, JMeter, autosplit]Trigger it with the config path, and add the download flags when you want results on the machine that started the run rather than only in the dashboard.
./hyperexecute --config hyperexecute.yaml \
--download-artifacts \
--download-artifacts-path ./dl \
--download-logsThe block registry further down breaks this file into five reusable pieces, so you can swap the runtime without touching discovery or change distribution without rewriting artifact capture.
Note: Performance runs draw on the same test-execution minutes as your functional suite, so there is no separate load-testing contract to negotiate. Upload an existing JMeter plan or Gatling simulation and get a result in one sitting. Try it free!
Gatling is the better-served of the two frameworks, because it has documented portal and CLI routes. Its portal flow is also more opinionated than JMeter's, since it asks you to declare what kind of test you are running before it asks for numbers.
Step 1: Create the project and upload the simulation

Step 2: Choose a test type
Select the simulation, click Run, and pick one of three test types. Each takes different parameters, and picking the wrong one produces a perfectly valid run that answers a question you did not ask.
| Test type | What it answers | Parameters it takes |
|---|---|---|
| Capacity Test | How far the application scales before it degrades | Duration (min), Initial Users, Final Users, expressed as user arrival rate per second |
| Stress Test | Where it crashes and whether it recovers | Duration (min), Total Injected Users |
| Soak Test | Whether it degrades over extended production-like use | Duration (min), Constant User Arrival Rate |
The Test Load Criteria panel opens as step 1 of 2, and each parameter has its own toggle, so a field left switched off is simply not applied. Capacity Test asks for a duration and the initial and final user arrival rates per second.

Stress Test replaces those arrival rates with a single total injected user count, which is the population pushed at the system to find its breaking point.

Soak Test holds a constant user arrival rate for the duration, which is the only one of the three that surfaces a slow memory leak.

The distinction between Capacity and Stress is worth getting right. Capacity ramps between two arrival rates to find the degradation point, while Stress injects a total population to find the breaking point, and a memory leak shows up in Soak rather than in either of the other two.
Step 3: Configure load distribution and limits

Click Run Test to start the job.
Step 4: Read the results
A finished Gatling run appears in the Jobs section with five tabs, and the last one is the one most teams actually want.

One authoring detail decides whether the portal fields do anything at all. Your simulation should read its load values from system properties rather than hardcoding them, so the values you type into the form reach the code.
LoadSimulation.java
// Read load parameters from system properties so the portal fields drive the run
int users = Integer.getInteger("users", 10);
int duration = Integer.getInteger("duration", 60);
String injectType = System.getProperty("injectType", "constant");
String workloadModel = System.getProperty("workloadModel", "open");A simulation with hardcoded injection values still runs, and the portal will still accept your numbers, but the run ignores them. That failure is silent, which makes it worth checking before your first real measurement.
Gatling runs through Maven, so the config resolves dependencies in the pre step and hands execution to the Maven plugin. The application under test can be started as a background service so the simulation has a target.
hyperexecute.yaml
---
version: 0.1
runson: linux
autosplit: true
concurrency: 1
scenarioCommandStatusOnly: true
runtime:
- language: java
version: "17"
background:
- mvn spring-boot:run -Dspring-boot.run.main-class=dev.simonverhoeven.gatlingdemo.GatlingDemoApplication || true
pre:
- mvn -Dmaven.repo.local=./.m2 dependency:resolve
testDiscovery:
type: raw
mode: static
command: echo "Test"
testRunnerCommand: mvn gatling:test
uploadArtefacts:
- name: TestReport
path:
- target/gatling/**
retryOnFailure: true
maxRetries: 1Two details in that file look wrong and are not. The discovery command is deliberately trivial because Maven already knows which simulations to run, so discovery only needs to emit one entity for Auto Split to allocate.
The || true on the background service keeps a non-zero exit from that long-running process out of the job result. The Gatling on HyperExecute documentation covers the portal fields and the sample project this config is drawn from.
Note the runtime shape. Gatling uses the list form with language and version, while k6 uses a map with an addons key, and merging the two produces a config that fails validation.
Most teams end up using both, because they solve different problems. The portal answers a question today and the CLI answers it every night without anyone remembering to ask.
| Consideration | Upload and run | CLI and YAML |
|---|---|---|
| Setup cost | Upload a file and fill a form | Author a config file and install a binary |
| Runs unattended | No, it needs a browser session | Yes, on a schedule or a pipeline trigger |
| Regional load split | Built into the form as a percentage per region | Not exposed as a YAML key |
| Version control | Configuration lives in the portal, not the repository | Configuration is a file, reviewed like any other change |
| Best first move | Proving the plan works and getting a baseline | Turning that baseline into a nightly regression check |
The regional split row is the one that decides architecture. If load has to originate from several geographies, the portal is the documented way to configure that, so a CLI-only setup means giving up geographic distribution.
Authoring the YAML is the slowest part of the CLI path, because discovery and runner commands depend on how your repository is laid out. TestMu AI publishes an open-source HyperExecute skill that hands that job to an AI coding agent.
git clone https://github.com/LambdaTest/agent-skills.git .claude/skills/agent-skillsIt ships four helper scripts that are runnable on their own, with or without an agent. doctor.js checks readiness, validate-config.js lints the YAML offline, build-command.js prints the correct CLI invocation, and summarize-artifacts.js triages a downloaded artifact tree.
The readiness check is the one worth running first in a new repository or a failing CI runner, because it names what is missing rather than just failing:
doctor.js output
$ node scripts/doctor.js --config hyperexecute.yaml
OK: HyperExecute CLI found at ./hyperexecute
OK: HyperExecute CLI is executable
OK: Config file found at hyperexecute.yaml
OK: LT_USERNAME is set
OK: LT_ACCESS_KEY is set
WARN: No .hyperexecuteignore found. Consider excluding secrets, local artifacts, and unrelated monorepo files.
WARN: No .gitignore found. Ensure local secrets are not committed.
HyperExecute doctor finished without blocking issues.That last warning is worth acting on. Without a .hyperexecuteignore the CLI packages the whole working directory into the uploaded payload, which on a monorepo means slow uploads and a real risk of shipping local secrets to the grid.
The skill also refuses to trigger a paid cloud job without asking, unless you explicitly opt into an autonomous session. That matters when an agent is holding keys that spend execution minutes.
The two builds later in this guide are assembled from the five blocks below. Each carries the prompt that regenerates it against your own repository, the YAML it produces, and a line stating what was actually verified.
Declares the language stack installed before anything else runs. JMeter and Gatling need a JVM, k6 does not.
Prompt
Using the HyperExecute skill, add a runtime block to hyperexecute.yaml that
installs Java 17 on a Linux node, then add pre steps that download Apache
JMeter 5.6.3 from the Apache archive, extract it, and create a results
directory. Use the list form of the runtime key.hyperexecute.yaml
runson: linux
runtime:
- language: java
version: "17"
pre:
- wget -q https://archive.apache.org/dist/jmeter/binaries/apache-jmeter-5.6.3.tgz
- tar -xzf apache-jmeter-5.6.3.tgz
- mkdir -p resultsVerified: the pre stage completed in 5s on the first machine and 2s on the second, the difference being dependency caching between tasks.
Emits the list of things to split across machines. Any shell command that prints one entity per line works.
Prompt
Add a testDiscovery block that lists every .jmx file in the plans directory,
using raw type and static mode. Then add a testRunnerCommand that runs one
discovered plan in JMeter non-GUI mode, writing the results file and the
JMeter log into the results directory. Remember the $test placeholder.hyperexecute.yaml
testDiscovery:
type: raw
mode: static
command: ls plans/*.jmx
testRunnerCommand: ./apache-jmeter-5.6.3/bin/jmeter -n -t $test -l results/result.jtl -j results/jmeter.logVerified: discovery returned two plans and completed in 0s on both machines. The $test placeholder is mandatory, and a runner command missing it produces a job that starts machines and runs nothing.
Both machines write to the same filename without colliding, because each task runs on its own isolated virtual machine. Per-task output paths do not need unique names, which is a common over-engineering trap when moving from a single load generator.
Distributes discovered entities across a pool of machines. This is the block that decides wall-clock time.
Prompt
Enable the auto-split distribution strategy with a concurrency of 2, and set
the YAML version to 0.1 so no framework directive is required. Explain in a
comment what happens if concurrency exceeds the number of discovered tests.hyperexecute.yaml
version: 0.1
autosplit: true
concurrency: 2 # capped at the discovered-entity count; extra machines are not provisionedVerified: requesting concurrency 3 against two discovered plans printed Concurrency 3 is getting overwritten by 2 and provisioned two machines. That is the platform declining to start an idle machine, not an error.
The version number matters more than it looks. On version: 0.2 the same config is rejected with framework name is required under framework directive, so a v0.2 file needs the block below while v0.1 needs nothing.
Required only on version 0.2
framework:
name: jmeterTesting the accepted values one at a time, jmeter, generic, and custom all validate, while raw is rejected with PlatformName is required in case of raw framework.
Decides what survives the machine being destroyed. Skip this block and your results file dies with the VM.
Prompt
Add report generation and artifact upload for everything under the results
directory, name the artifact jmeter-results, add a post step that tails the
results file, set scenarioCommandStatusOnly so the generic runner command
reports status correctly, and tag the job with performance labels.hyperexecute.yaml
post:
- tail -5 results/result.jtl
scenarioCommandStatusOnly: true
report: true
partialReports:
location: results
type: html
uploadArtefacts:
- name: jmeter-results
path:
- results/**
jobLabel: [Performance, JMeter, autosplit]Verified: this block produced a downloadable artifact tree of two results files and two JMeter logs, one pair per task, sized in the summarize-artifacts.js output later in this guide. Note the British spelling of uploadArtefacts, which is a genuine source of silent misconfiguration.
The scenarioCommandStatusOnly key deserves its own note, because leaving it out produces the most confusing status in HyperExecute. A load-tool invocation is a generic command with no framework binding, so HyperExecute cannot read scenario-level results from it and marks the job PARTIALLY COMPLETED even when every stage passed and no task failed.
Setting it true tells HyperExecute to judge the scenario by the command exit status instead, which took an otherwise identical job from PARTIALLY COMPLETED to COMPLETED. The Gatling and k6 configurations in this guide both carry the key, and the troubleshooting table separates it from the unrelated failure where a stage reports no passes at all.
Bounds the blast radius. A load test that hangs is worse than one that fails, because it burns the daily execution allowance while telling you nothing.
Prompt
Add a global timeout of 30 minutes, retry a failed test command once, and
enable fail-fast so the job aborts after three consecutive failures instead
of running the whole suite against a broken environment.hyperexecute.yaml
globalTimeout: 30 # minutes, 1 to 150, default 90
retryOnFailure: true
maxRetries: 1
failFast:
maxNumberOfTests: 3Verified: accepted by CLI validation as part of the nightly build. Keep maxRetries low on load tests, because a retry re-applies load and a flapping environment can be pushed further by the retry itself.
The goal here is speed, not depth. This build answers one question, whether the change made things obviously worse, and it has to finish fast enough that reviewers do not route around it.
Prompt
Using the HyperExecute skill, write a complete hyperexecute.yaml for a
pull-request performance smoke test. YAML version 0.1 on Linux, auto-split
with concurrency 1, Java 17, download and extract Apache JMeter 5.6.3, run
only plans/smoke.jmx, upload the results directory as an artifact, and cap
the job at 10 minutes. Then validate it with the official CLI.hyperexecute.yaml
---
version: 0.1
runson: linux
autosplit: true
concurrency: 1
globalTimeout: 10
runtime:
- language: java
version: "17"
pre:
- wget -q https://archive.apache.org/dist/jmeter/binaries/apache-jmeter-5.6.3.tgz
- tar -xzf apache-jmeter-5.6.3.tgz
- mkdir -p results
testDiscovery:
type: raw
mode: static
command: ls plans/smoke.jmx
testRunnerCommand: ./apache-jmeter-5.6.3/bin/jmeter -n -t $test -l results/result.jtl -j results/jmeter.log
post:
- tail -5 results/result.jtl
scenarioCommandStatusOnly: true
report: true
partialReports:
location: results
type: html
uploadArtefacts:
- name: smoke-results
path:
- results/**
jobLabel: [Performance, JMeter, smoke, pr]Keep the plan itself small. A smoke test built on 10 threads with a 5 second ramp is enough to catch an order-of-magnitude regression, and it does not need to catch a real capacity limit because that is Build 2's job.
This build splits every plan in the directory across machines, retries once on failure, and keeps the artifacts. It produced the console output and response-time numbers in the next two sections.
Prompt
Extend the smoke config into a nightly load run. Discover every .jmx file in
the plans directory instead of one file, raise concurrency to 2, add fail-fast
after 3 consecutive failures, retry once on failure, raise the global timeout
to 30 minutes, and label the job for nightly performance runs. Validate it,
then show me the run command with artifact and log download enabled.hyperexecute.yaml
---
version: 0.1
runson: linux
autosplit: true
concurrency: 2
globalTimeout: 30
retryOnFailure: true
maxRetries: 1
failFast:
maxNumberOfTests: 3
runtime:
- language: java
version: "17"
pre:
- wget -q https://archive.apache.org/dist/jmeter/binaries/apache-jmeter-5.6.3.tgz
- tar -xzf apache-jmeter-5.6.3.tgz
- mkdir -p results
testDiscovery:
type: raw
mode: static
command: ls plans/*.jmx
testRunnerCommand: ./apache-jmeter-5.6.3/bin/jmeter -n -t $test -l results/result.jtl -j results/jmeter.log
post:
- tail -5 results/result.jtl
scenarioCommandStatusOnly: true
report: true
partialReports:
location: results
type: html
uploadArtefacts:
- name: jmeter-results
path:
- results/**
jobLabel: [Performance, JMeter, autosplit, nightly]Validate before spending anything. The validate flag checks schema without provisioning machines, which turns a mid-pipeline failure into an instant local one:
$ ./hyperexecute --validate --config hyperexecute.yaml
Generating TraceID for tracking request: 01KZTPWZK4BQABG5SX8Z7N95TB
HyperExecute Config validated successfullyWiring this into a scheduled pipeline needs no plugin, because the CLI is one binary that takes a config path and returns an exit code.
.github/workflows/performance.yml
name: Performance
on:
schedule:
- cron: '0 2 * * *'
workflow_dispatch:
jobs:
load-test:
runs-on: ubuntu-latest
env:
LT_USERNAME: ${{ secrets.LT_USERNAME }}
LT_ACCESS_KEY: ${{ secrets.LT_ACCESS_KEY }}
steps:
- uses: actions/checkout@v4
- name: Download HyperExecute CLI
run: |
curl -O https://downloads.lambdatest.com/hyperexecute/linux/hyperexecute
chmod +x hyperexecute
- name: Validate config
run: ./hyperexecute --validate --config hyperexecute.yaml
- name: Run load test
run: |
./hyperexecute --config hyperexecute.yaml \
--download-artifacts \
--download-artifacts-path ./perf-results \
--labels "ci,nightly" \
--no-track
- uses: actions/upload-artifact@v4
if: always()
with:
name: perf-results
path: ./perf-resultsThe --no-track flag stops progress streaming, which matters in CI because the progress bar redraws constantly and turns the log into thousands of unreadable lines. For broader pipeline patterns, optimizing CI/CD pipelines with HyperExecute covers the same binary driving functional suites.
Build 2 was run against two JMeter plans, each generating 10 threads with a 5 second ramp-up against the ecommerce playground. The CLI opens by echoing back its interpretation of your config, which is the fastest way to catch a misread key.
Job start
Execution Plan
Mode: autosplit
Runson: linux
Concurrency: 2
Fail Fast:
Max Number of Tests: 3
Report: enabled
Artefacts:
jmeter-results: results/**
Test Discovery Result: 2
plans/checkout.jmx
plans/search.jmx
Job 102bd327-333d-4dfa-9a9b-47a326e06056 has started RUNNING
Job Link: https://hyperexecute.lambdatest.com/hyperexecute/task?jobId=102bd327-333d-4dfa-9a9b-47a326e06056Each machine then works through its own stage sequence, and the bracketed number identifies the task. Seeing both tasks progress independently confirms the plans genuinely ran in parallel rather than in sequence.
Stage progress
[1] setup-runtime (3s)
[2] setup-runtime (3s)
[1] pre (6m30s)
[2] pre (8m55s)
[1] discovery (0s)
[1] plans/checkout.jmx (11s)
[1] post (0s)
[2] discovery (0s)
[2] plans/search.jmx (10s)
[2] post (0s)The closing summary separates two numbers that are easy to conflate:
Job summary
COMPLETED
Test Execution Time: 2m34s
Job Duration Time: 9m21s
Total Tasks: 2
Cumulative Time Consumed: 16m17s
Total Stages: 8
Pass discovery stage percentage: 100.00%
Pass pre stage percentage: 100.00%
Pass test stage percentage: 100.00%
Pass post stage percentage: 100.00%
Failed Tasks: 0Cumulative Time Consumed is machine time, the figure your execution allowance is drawn against. Job Duration Time is wall-clock, the figure your pipeline waits on, and on this run 16m17s of machine time was delivered in 9m21s of waiting because two machines worked at once.
This particular run is also a fair warning about pinning dependencies to a public mirror. The two pre stages took 6m30s and 8m55s downloading JMeter from the Apache archive, which is why a 21 second test run sat inside a nine minute job, and it is the strongest argument in this guide for caching that download between runs.
Gate on the artifact rather than the exit code. A JMeter run whose requests mostly returned errors still finishes as a successful job, because the runner executed correctly even though the system under test did not.
Artifacts arrive grouped by task number, so a two-machine run produces two independent results files. The skill's summarize-artifacts.js gives you the shape of the tree before you open anything:
summarize-artifacts.js output
$ node scripts/summarize-artifacts.js ./dl
Artifact root: ...\hetest\dl
Directories: 5
Files: 4
Bytes: 27793
Extensions:
.jtl: 2
.log: 2
Interesting files:
jmeter-results\1\results\jmeter.log (8211 bytes)
jmeter-results\1\results\result.jtl (5669 bytes)
jmeter-results\2\results\jmeter.log (8213 bytes)
jmeter-results\2\results\result.jtl (5700 bytes)A .jtl file is CSV, so percentiles are a few lines of script rather than a reporting tool. This reads every task directory and exits non-zero when the p95 budget is breached:
parse-results.js
const fs = require('fs');
const all = [];
let failures = 0;
const pct = (arr, p) => arr[Math.floor(arr.length * p)];
for (const task of fs.readdirSync('dl/jmeter-results')) {
const lines = fs.readFileSync(`dl/jmeter-results/${task}/results/result.jtl`, 'utf8').trim().split('\n');
const header = lines[0].split(',');
const iElapsed = header.indexOf('elapsed');
const iSuccess = header.indexOf('success');
const times = [];
for (const line of lines.slice(1)) {
const cols = line.split(',');
if (cols.length < 3) continue;
times.push(Number(cols[iElapsed]));
if (cols[iSuccess] !== 'true') failures++;
}
times.sort((a, b) => a - b);
console.log('task', task, '| samples', times.length, '| p95', pct(times, 0.95), 'ms');
all.push(...times);
}
all.sort((a, b) => a - b);
console.log('combined | samples', all.length, '| failures', failures);
console.log('p50', pct(all, 0.5), '| p95', pct(all, 0.95), '| max', all[all.length - 1], 'ms');
if (pct(all, 0.95) > 1000) {
console.error('p95 regression: ' + pct(all, 0.95) + 'ms exceeds the 1000ms budget');
process.exit(1);
}Run against the artifacts from the job above, that script prints:
task 1 | samples 30 | p95 702 ms
task 2 | samples 30 | p95 848 ms
combined | samples 60 | failures 0
p50 330 | p95 848 | max 1140 msThe two machines ran identical request shapes against the same endpoint and still landed on different tail latencies. Set thresholds against the p95 of the slowest task rather than the mean across all of them, or one genuinely slow generator gets averaged into looking healthy.
Store the results file per run and compare against a rolling baseline, because an 848 ms p95 only means something next to last week's number. Failure clustering and flaky-run noise across builds are what Test Insights is built to surface.
Note: HyperExecute distributes tests with Matrix, Auto Split, and Hybrid strategies across just-in-time virtual machines, and the TestMu AI product page states up to 70% faster test execution than traditional grids as an upper bound rather than a per-suite guarantee. See how the orchestration cloud handles your existing suite on HyperExecute.
k6 is the third documented tool and is CLI-only, with no portal upload flow. It installs as a runtime addon with a pinned version rather than through a language runtime.
hyperexecute.yaml
---
version: "0.1"
runson: linux
autosplit: true
concurrency: 2
runtime:
addons:
- name: k6
version: "v0.52.0"
env:
K6_BROWSER_ENABLED: true
K6_BROWSER_HEADLESS: false
HE_CONTEXT_K6_SETUP_DEFAULT_BROWSER_PATH: true
pre:
- k6 version
testDiscovery:
type: raw
mode: dynamic
command: ls tests/*.js
testRunnerCommand: k6 run $test
scenarioCommandStatusOnly: true
jobLabel: [K6, 'HyperExecute', autosplit]The three environment variables are only needed for browser-mode runs. A protocol-level k6 script does not need them, and leaving them set on a non-browser run is harmless but misleading to the next reader.
If you have not settled on a tool yet, load testing tools compares the options, and the k6 testing tutorial covers writing the scripts themselves.
Nothing in the registry is performance-specific except the runner command. Swap that one line and the same five blocks orchestrate a functional suite, which is why teams usually end up with one config pattern rather than two toolchains.
| Goal | What changes | What stays |
|---|---|---|
| Functional suite | Runner command becomes your test command, discovery lists test classes or spec files | @autosplit, @artifacts, @gates unchanged |
| Same plan, many environments | Swap @autosplit for a matrix block and use testSuites instead of testRunnerCommand | @runtime, @artifacts, @gates unchanged |
| Many plans, many environments | Hybrid strategy, using parallelism instead of concurrency, discovery mode must be remote | @runtime, @artifacts unchanged |
| Soak test | Raise globalTimeout, split across machines rather than one long task | All five blocks, only values change |
The matrix variant removes concurrency arithmetic entirely. Declaring a two-entry environment list against a three-entry plan list produces six tasks, and the machine count follows automatically.
hyperexecute.yaml
version: 0.1
runson: linux
matrix:
env: ["staging", "preprod"]
plan: ["checkout.jmx", "search.jmx", "browse.jmx"]
testSuites:
- ./apache-jmeter-5.6.3/bin/jmeter -n -t plans/$plan -JtargetEnv=$env -l results/$env-$plan.jtlThe Auto Split strategy documentation gives the worked example for the other direction: 27 discovered scenarios with concurrency 7 allocates 7 nodes and runs the 27 across them in parallel.
Every row below is a failure hit while building the configs in this guide, or one the documentation calls out explicitly. They are listed by the symptom your terminal or dashboard actually shows.
| Symptom | Cause | Fix |
|---|---|---|
| framework name is required under framework directive | YAML version 0.2 requires a framework block that version 0.1 does not | Add a framework block naming the tool, or set version to 0.1 for a command-driven load run |
| PlatformName is required in case of raw framework | The raw framework name carries extra required fields | Name the actual tool instead, since jmeter, generic, and custom all validate without a platform key |
| A stage shows no passes with zero failed tasks | The post or artifact stage never ran successfully, usually because the declared path matched no files | Add mkdir -p for the output directory in pre and declare a post step. That moved the post stage from no passes to a full pass on an otherwise identical job |
| PARTIALLY COMPLETED despite every stage passing | A generic testRunnerCommand has no framework binding, so HyperExecute cannot map scenario-level results even though the command succeeded | Add scenarioCommandStatusOnly: true so the scenario is judged by command exit status. Two otherwise identical jobs confirm it: without the key the job ended PARTIALLY COMPLETED even at 100% stage pass, with it the job ended COMPLETED. Fixing the post stage alone does not change the status |
| Concurrency N is getting overwritten by M | Requested more machines than discovery found entities | Expected behavior rather than an error. Split plans more finely if you want the extra parallelism |
| Job runs but no tests execute | Discovery returned nothing, or testRunnerCommand is missing the $test placeholder | Run the discovery command locally and confirm it prints one entity per line |
| Gatling ignores the load numbers you typed | The simulation hardcodes its injection profile instead of reading system properties | Read users, duration, injectType, and workloadModel from system properties in the simulation |
| Load plateaus well below 2000 users | Load-generator resource limits, heavy request payloads, or too few machines | Raise machine count, spread across regions, and lighten per-request payloads |
| Payload upload is slow or leaks files | No .hyperexecuteignore, so the whole working directory is packaged | Add .hyperexecuteignore excluding local artifacts and secrets, which doctor.js warns about by default |
One capacity constraint is easy to hit on an aggressive schedule. Each organization has a maximum allowable daily test duration calculated from its provisioned parallel sessions, documented as 6 hours of runtime per parallel session per day.
Long sessions carry their own warning, since the documentation notes that runs exceeding 120 minutes can cause memory and CPU spikes. That is a direct argument for splitting a soak test across machines rather than running one very long task.
Start in the portal. Upload one existing .jmx plan or .java simulation, set a modest user count and a realistic ramp, and run it once to get a baseline you trust before automating anything.
Then reconcile the sample count in the results against what your plan should have generated. Getting that number right is what separates a capacity measurement from a guess, and it is the step teams most often skip.
Once one run is trustworthy, move it to Build 1 as a pull-request check and Build 2 as a nightly schedule, keeping every artifact for trend comparison. The getting started with HyperExecute documentation covers account setup and first-job specifics.
Author
Anmol Gupta is Vice President of Product Management at TestMu AI (formerly LambdaTest), driving HyperExecute, the test orchestration cloud that runs and accelerates automated test execution. He led the development of the Unified Test Execution Cloud Platform and now leads a 30-member cross-functional product organization across product lines contributing $7M+ in revenue. He brings over nine years of experience and previously co-founded the SaaS company Timble as CTO, where he grew the team from 5 to 40 and launched an AI KYC platform that processed 600K+ applications in five months while cutting verification time from 12 minutes to under 30 seconds. Anmol holds an MTech and BTech from IIT Delhi.
Reviewer
Japneet Singh Chawla is an Engineering Manager at TestMu AI (formerly LambdaTest), where he leads a team driving HyperExecute, the AI-native Test Orchestration Cloud Platform, and integrations with Cypress, Provar, Tosca, and Selenium, improving test execution efficiency and driving adoption across 500+ enterprise clients. He also spearheaded zero-downtime deployments that cut release-related downtime by 90%, and mentors new engineers into productive contributors. He brings 9+ years of experience building and scaling distributed systems, SaaS platforms, and developer tools, with deep hands-on backend engineering across Golang, Python, Node.js, Kafka, and Redis. Earlier at Sumo Logic he built award-winning developer tools, including a VS Code Parser Linter, and at Indus Valley Partners he was a founding member of the Sentiment Analyzer team, building ML-powered solutions for financial clients. Japneet holds an MCA in Computer Science from GGSIPU.
Did you find this page helpful?
TestMu AI forEnterprise
Get access to solutions built on Enterprise
grade security, privacy, & compliance