Hero Background

Next-Gen App & Browser Testing Cloud

Trusted by 2 Mn+ QAs & Devs to accelerate their release cycles

Next-Gen App & Browser Testing Cloud
HyperExecutePerformance Testing

Run Performance Tests at Scale

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

Author

Anmol Gupta

Author

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

  • Load replication: On any HyperExecute multi-generator setup, the thread count inside a .jmx file runs on every machine in every region. A 250-user plan on 3 machines across 2 regions generates 1,500 concurrent users, not 250, unless overrides are set.
  • Load infrastructure: HyperExecute provisions virtual machines per task and destroys them when the job ends, so there is no load-generator fleet to provision or maintain.
  • Regional origin: HyperExecute generates load from six documented regions. Default region: East US, in Richmond, Virginia. A test that never sets this measures latency from Virginia regardless of where the users are.
  • Running tests manually: HyperExecute runs a JMeter .jmx plan or a Gatling .java simulation uploaded through its Projects dashboard, with load configured in a form. No configuration file and no CLI are required.
  • Automating tests in CI: HyperExecute also runs the same tools from a hyperexecute.yaml file triggered by a single binary, which is the path that works unattended in a CI pipeline.
  • Tool support: JMeter yes, Gatling yes, k6 yes, Locust no. Locust is not documented anywhere in the HyperExecute docs and should be treated as unsupported.

Which Path Should You Start With?

  • Portal upload: Runs unattended, no. It needs a browser session, which makes it the right choice for a first manual baseline.
  • CLI and hyperexecute.yaml: Runs unattended, yes. A single binary takes a config path and returns an exit code, so a schedule or pipeline trigger can drive it nightly.

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.

What Breaks When You Scale a Load Test?

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.

  • Without overrides - A plan specifying 250 users, run on 3 machines across 2 regions, generates 250 x 3 x 2 for a real total of 1,500 concurrent users against your endpoint.
  • With overrides - A target of 500 users across 2 regions with a 100-user-per-machine cap resolves to 3 machines per region carrying roughly 84, 83, and 83 users each, summing to the 500 you asked for.
  • Why it hurts twice - The first failure is a false capacity signal, because the system was never tested at the level you recorded. The second is a false pass, when a plan you believed was heavy got split so thin it exercised nothing.

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.

  • West US 2 - Moses Lake, Washington.
  • East US - Richmond, Virginia, and the default when nothing is configured.
  • Central India - Pune, Maharashtra.
  • Southeast Asia - Singapore.
  • Brazil South - Sao Paulo State, Brazil.
  • Mexico Central - Queretaro State, Mexico.

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.

What Do You Need Before You Start?

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.

  • For JMeter - A .jmx test plan authored in the Apache JMeter GUI. Standard Thread Group is supported by default, and custom thread groups also work.
  • For Gatling - Simulation files in .java format, ideally reading their load values from system properties so the portal fields can drive them.
  • For the CLI path only - The HyperExecute binary and your credentials exported as environment variables.
# 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.351

Running JMeter Load Tests

JMeter 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.

Method 1: Upload a .jmx Plan and Run 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

  • Open the HyperExecute Projects dashboard.
  • Click New Project.
  • Browse to your JMeter test files and upload them.
  • Click Save.

Step 2: Configure the load

  • Select the .jmx file you want to run.
  • Click Run.
  • Fill in the configuration fields described below.
  • Click Continue, then click Run Test.

Each field changes what your results actually mean, so it is worth knowing what you are setting rather than accepting defaults.

FieldWhat it controlsPractical note
Total UsersThe number of users you intend to test forRead the load math section before trusting this figure, because it interacts with machine count
Duration (min)How long the test runsPair 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 loadA realistic ramp separates a real capacity limit from a cold-start artifact
Total Load DistributionThe regions load is generated from, as a percentage of users per regionDefaults to East US, so untouched tests measure latency from Virginia
Machine countThe number of machines used for parallel test executionThis is the multiplier in the load math trap
Split CSVSplits input data from a CSV file among different threads or regionsWithout it every generator replays identical test data
Java VersionThe runtime the plan executes onJava 11 is the default and Java 25 is available
Job LabelsTags applied to the execution for identificationMakes 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.

  • jmeter.save.saveservice.subresults - Setting it false reduces the results file size, which matters a lot on plans with many embedded resources.
  • httpclient.socket.https.cps - Simulates constrained bandwidth, where 0 means unlimited. This is how you model a mobile network rather than a datacentre connection.

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.

HyperExecute JMeter Summary report showing virtual users, response time, throughput, error rate and regional load distribution

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.

HyperExecute JMeter Timeline report plotting performance across the duration of the run

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

HyperExecute JMeter Request Stats view breaking down request volume and timing per request

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.

HyperExecute JMeter Errors view listing error codes with counts and percentages

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

HyperExecute JMeter Logs view showing raw execution output for the performance job

Method 2: Run JMeter From the CLI

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-logs

The 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

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!

Running Gatling Load Tests

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.

Method 1: Upload a Simulation and Run It

Step 1: Create the project and upload the simulation

  • Open the HyperExecute Projects dashboard and click New Project.
  • Select Gatling as the performance testing framework.
  • Browse and upload your Gatling simulation files in .java format.
  • Click Save.
HyperExecute Projects dashboard with a Gatling performance project and its uploaded simulation files

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 typeWhat it answersParameters it takes
Capacity TestHow far the application scales before it degradesDuration (min), Initial Users, Final Users, expressed as user arrival rate per second
Stress TestWhere it crashes and whether it recoversDuration (min), Total Injected Users
Soak TestWhether it degrades over extended production-like useDuration (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.

HyperExecute Test Load Criteria panel configuring a Gatling Capacity Test with duration, initial users and final users

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.

HyperExecute Test Load Criteria panel configuring a Gatling Stress Test with duration and total injected users

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.

HyperExecute Test Load Criteria panel configuring a Gatling Soak Test with duration and constant user arrival rate

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

  • Region - The geographic origin of the load, defaulting to East US.
  • % of Traffic - The share of load generated from each selected region.
  • Max Users per Engine - The per-engine virtual user cap, which defaults to 2000.
  • Global Timeout - The maximum job duration, which defaults to 90 minutes.
  • Job Labels - Identification tags for the run.
HyperExecute load distribution step for a Gatling run, setting region, percentage of traffic, max users per engine and global timeout

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.

  • Job Summary - Duration, execution time, and status.
  • Scenarios - Per-scenario execution validation.
  • Logs - Maven build output alongside the Gatling simulation output.
  • Artifacts - The Gatling HTML reports as files.
  • Report - The consolidated HTML report, downloadable in one click.
HyperExecute Jobs view for a completed Gatling run showing job summary, scenarios, logs, artifacts and report tabs

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.

Method 2: Run Gatling From the CLI

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: 1

Two 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.

Which Method Should You Use?

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.

ConsiderationUpload and runCLI and YAML
Setup costUpload a file and fill a formAuthor a config file and install a binary
Runs unattendedNo, it needs a browser sessionYes, on a schedule or a pipeline trigger
Regional load splitBuilt into the form as a percentage per regionNot exposed as a YAML key
Version controlConfiguration lives in the portal, not the repositoryConfiguration is a file, reviewed like any other change
Best first moveProving the plan works and getting a baselineTurning 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.

Let an Agent Write the Config

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-skills

It 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.

Run tests up to 70% faster on the TestMu AI cloud grid

The Block Registry

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.

@runtime

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 results

Verified: the pre stage completed in 5s on the first machine and 2s on the second, the difference being dependency caching between tasks.

@discovery

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.log

Verified: 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.

@autosplit

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 provisioned

Verified: 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: jmeter

Testing 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.

@artifacts

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.

@gates

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: 3

Verified: 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.

Build 1: The Pull-Request Smoke Test

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.

Build 2: The Nightly Load Run

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 successfully

Wiring 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-results

The --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.

Console Output From a Real Run

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-47a326e06056

Each 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:                         0

Cumulative 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.

Reading the Results and Gating the Build

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 ms

The 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

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.

Running k6 Suites

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.

What Else Can These Same Blocks Do?

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.

GoalWhat changesWhat stays
Functional suiteRunner command becomes your test command, discovery lists test classes or spec files@autosplit, @artifacts, @gates unchanged
Same plan, many environmentsSwap @autosplit for a matrix block and use testSuites instead of testRunnerCommand@runtime, @artifacts, @gates unchanged
Many plans, many environmentsHybrid strategy, using parallelism instead of concurrency, discovery mode must be remote@runtime, @artifacts unchanged
Soak testRaise globalTimeout, split across machines rather than one long taskAll 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.jtl

The 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.

Troubleshooting

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.

SymptomCauseFix
framework name is required under framework directiveYAML version 0.2 requires a framework block that version 0.1 does notAdd 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 frameworkThe raw framework name carries extra required fieldsName the actual tool instead, since jmeter, generic, and custom all validate without a platform key
A stage shows no passes with zero failed tasksThe post or artifact stage never ran successfully, usually because the declared path matched no filesAdd 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 passingA generic testRunnerCommand has no framework binding, so HyperExecute cannot map scenario-level results even though the command succeededAdd 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 MRequested more machines than discovery found entitiesExpected behavior rather than an error. Split plans more finely if you want the extra parallelism
Job runs but no tests executeDiscovery returned nothing, or testRunnerCommand is missing the $test placeholderRun the discovery command locally and confirm it prints one entity per line
Gatling ignores the load numbers you typedThe simulation hardcodes its injection profile instead of reading system propertiesRead users, duration, injectType, and workloadModel from system properties in the simulation
Load plateaus well below 2000 usersLoad-generator resource limits, heavy request payloads, or too few machinesRaise machine count, spread across regions, and lighten per-request payloads
Payload upload is slow or leaks filesNo .hyperexecuteignore, so the whole working directory is packagedAdd .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.

Conclusion

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.

Test infrastructure that does not break, from TestMu AI

Author

...

Anmol Gupta

Blogs: 3

  • Linkedin

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

Reviewer

  • Linkedin

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.

Add to Google preferred sources

Summarise with AI

Copied to Clipboard!
...

3000+ Browsers. One Platform.

See exactly how your site performs everywhere.

Try it free
...

Write Tests in Plain English with KaneAI

Create, debug, and evolve tests using natural language.

Try for free

HyperExecute Performance Testing FAQs

Did you find this page helpful?

TestMu AI forEnterprise

Get access to solutions built on Enterprise
grade security, privacy, & compliance

  • Advanced access controls
  • Advanced data retention rules
  • Advanced Local Testing
  • Premium Support options
  • Early access to beta features
  • Private Slack Channel
  • Unlimited Manual Accessibility DevTools Tests