Load Test Step Limits

A load test runs many virtual users in parallel, each firing requests as fast as the test allows. To keep that path fast and predictable, load test step scripts (Pre-Script and Post-Script on a step inside a spec) run in a smaller, sandboxed JavaScript engine instead of the full engine used everywhere else. This sub-section collects every limit that applies to that engine so you know what to expect.

These limits apply only to step scripts in a load test. Load test Before All and After All scripts run on the full engine and are not subject to any of the restrictions below.

Globals You Do Not Get

  • No setTimeout: artificial delays are not available. The test runner already controls pacing through the test-type configuration.
  • No WebView globals: crypto, URL, URLSearchParams, TextEncoder, TextDecoder, atob, and btoa are not defined here. Only the JavaScript language itself is.
  • Nothing genuinely asynchronous: the engine removes await from the script before running it, so writing await fetch(...) is fine, but a real Promise is never waited on. The script runs start to finish synchronously.

Slightly Reduced at Surface

  • at.environment, at.flow, at.request, at.response: all work, with the same methods and the same semantics (pre mutable / post read-only) as the main engine.
  • at.response.timing exposes only the total field. The dns, tls, and ttfb breakdown is not available inside load test step scripts.
  • at.export is not present. Export only makes sense after a run has finished, so it lives in After All scripts, where the full engine is back.

What Still Works

Despite the limits above, the engine is a real JavaScript engine and the day-to-day script patterns all work:

  • console.log / info / warn / error: all four methods work, with the same 1000-character total cap.
  • Plain JavaScript: variables, control flow, Math, JSON.parse / JSON.stringify, string and array helpers, regular expressions.
  • fetch(url, options): a step script can make its own HTTP call, reusing the same connection plumbing as the step's own requests. It takes method, headers, and body, and returns a plain object with status, ok, headers, body, json(), and text(). It is not the browser Response, and the body is always text.
  • at.flow.set / get: each virtual user gets its own flow store, fresh at the start of every iteration, so values set in a step are visible to that same virtual user's later steps in the same iteration.
  • at.environment.set: writes are local to that virtual user's current iteration. They do not reach other virtual users, do not carry into the next iteration, and never touch your saved environment. Treat it as scratch space, not as a way to share state across the run.

When You Need Something the Step Engine Cannot Do

A step script runs once per virtual user per iteration, so anything that only needs to happen once for the whole run belongs in a lifecycle script instead:

  • Seeding test data or fetching a tokenBefore All. Doing it in a step script would repeat the call for every virtual user on every iteration. Make it once with await fetch, store the result in at.environment or at.flow, and every step will have access to it.
  • Sending a report or notificationAfter All. Build the payload from at.export.* and post it wherever it needs to go.
  • Cleaning up the test user or test dataAfter All with fetch.