Skip to content

Change virtual users mid-run

A running test is not fixed. From the run’s Live tab you can move the virtual user target and the throughput cap while the load is in flight, pause and resume it, and every change is recorded on the run’s timeline with who made it and when.

Nothing restarts. The run keeps its id, its result series and its report.

  • The run must be running. A queued or finished run answers 409 RUN_NOT_RUNNING.
  • You need the controller role on the run (account members and test editors have it).
  • Your executor and your script must support the control. JMeter, k6, Locust and Gatling can move virtual users live; the other executors cannot, and the console says so instead of pretending. Except on Locust, the script also has to be built for it — a Concurrency Thread Group on JMeter, an externally-controlled scenario on k6, the helper opt-in on Gatling. Make your test controllable mid-run is the recipe per engine; What each executor supports at runtime is the full matrix.
  1. Open the run and select the Live tab.

  2. Set the Virtual users field to the new fleet-wide total and commit it.

    The number you type is always the whole fleet. Underneath the field MaxoPerf shows the divide it will perform — for example 2 000 = 20 runners × 100 — so what each runner is told is never a guess.

  3. Watch the ack pill. It counts runners that have confirmed the change: applied 4 987 / 5 000. It normally converges in well under a second.

  4. Confirm it landed in the load. The run’s charts get a vertical marker at the moment the change applied, so a step in the VU or throughput series lines up with the change that caused it.

Identical flow, using the Requests per second field. 0 means unlimited.

The Pause toggle stops the load without ending the run — useful when you want to fail something over, take a snapshot, or let a queue drain, and then continue in the same run and the same charts. Resume restores the previous target.

By default a change applies to every live runner. The scope chips narrow it:

ScopeUse it for
Allthe whole fleet (default)
Locationsone cloud region, or one private datacenter
Runnersnamed individual runners

A scope that resolves to zero live runners is rejected with 422 SELECTOR_NO_MATCHING_RUNNERS rather than accepted and quietly applied to nothing.

Increases are capped by your plan’s maxLiveConcurrentVus and maxLiveThroughputRps; going past them returns 403 PLAN_LIMIT_EXCEEDED. Enterprise sets both to -1 (unlimited), so no mid-run increase is refused on that plan.

Decreases are never blocked — including from a run that started above the cap. If your test is melting the system under test, you can always turn it down.

Terminal window
curl -X PUT "$MAXOPERF_API/v1/runs/$RUN_ID/live-controls/virtual-users" \
-H "Authorization: Bearer $MAXOPERF_API_KEY" \
-H "X-Account-Id: $ACCOUNT_ID" \
-H 'Content-Type: application/json' \
-d '{"scope":{"type":"all"},"total":2000}'
// 202 Accepted
{ "revision": 7, "perRunner": 100, "runnerCount": 20, "dispatchState": "dispatched", "delivered": true }

The per-runner share is rounded up and never drops below 1, so a small total across a wide fleet cannot be delivered exactly. When that happens the response says so rather than implying an exact split, and the timeline renders the total as approximate:

// 202 Accepted — 7 RPS across 20 runners is 1 each, so the fleet will really run 20
{ "revision": 8, "perRunner": 1, "runnerCount": 20, "clamped": true, "effectiveTotal": 20, }

clamped is absent whenever the divide is exact. If a decrease comes back clamped, the fleet is too wide for the target — release runners, or accept effectiveTotal as the floor.

Read the outcome back — desired state, capabilities and ack tallies — with GET /v1/runs/:runId/live-controls, the full change log with GET /v1/runs/:runId/live-controls/history, and the exact runners that have not applied a revision with GET /v1/runs/:runId/live-controls/revisions/:revision/acks?status=failed.