LOADRUNNER TO JMETER AND K6

What each VuGen construct becomes — and what does not carry across
v0.2
The mental model
LoadRunner is three programs — VuGen writes the script, Controller runs the scenario, Analysis reads the results. JMeter is one: the test plan holds the script and the load profile, and the HTML dashboard is generated from the results file afterwards. k6 is a CLI and a JavaScript file: the script is the scenario, and the summary prints when it finishes. The other shift is order. In VuGen a correlation is registered before the request that produces the value (web_reg_save_param then web_submit_data). In JMeter the extractor is a child of the sampler whose response it reads; in k6 you read res.body after the call. Same idea, opposite direction on the page.
LoadRunnerJMeterk6
vuser_init()setUp Thread Group (runs once, before) — or the first steps of the Thread Group with a Once Only Controllerexport function setup() — runs once, its return value is passed to every VU
Action()Thread Group — the loop body; one iteration per loopexport default function () — one call per iteration
vuser_end()tearDown Thread Groupexport function teardown(data)
Multiple Actions, weightedSeveral Thread Groups, or a Throughput Controller per action with percent executionSeveral scenarios in options.scenarios, each with its own exec function — there is no weighting field; the weight is each scenario's own vus or arrival rate
Controller scenario (Vusers, ramp, duration)Thread Group: threads, ramp-up, loop count / scheduler durationoptions.stages, or a ramping-vus executor
Goal-oriented scenario (hits/sec)Constant Throughput Timer, or Concurrency Thread Group + Throughput Shaping Timer (plugins)constant-arrival-rate / ramping-arrival-rate executor — this is native in k6 and better than either LoadRunner or JMeter at it
Run-time settings → IterationsLoop Count on the Thread Groupiterations in the executor, or per-vu-iterations
Requests
LoadRunnerJMeterk6
web_url()HTTP Request, method GEThttp.get(url, params)
web_submit_data()HTTP Request, POST, parameters in the body table (ITEMDATA → one row each)http.post(url, { field: value }, params) — an object body is form-encoded
web_custom_request()HTTP Request, any method, Body Data — the raw Body= string goes in verbatimhttp.request(method, url, body, params)
web_submit_form(), web_link(), web_image()Does not map. These are HTML-mode calls that find the target in the previous response. Re-record in URL mode, or find the underlying request in the recording log and write it as an HTTP RequestDoes not map — same reason. parseHTML(res.body) can find the link, but you are rewriting, not converting
web_add_header() / web_add_auto_header()HTTP Header Manager — as a child of one sampler, or at Thread Group scope for auto headersparams.headers = {…}, or set once in a shared object
web_set_user()HTTP Authorization Manager (Basic/Digest); for NTLM/Kerberos see the manager's mechanism fieldPut credentials in the URL, or params.auth — documented values are basic, digest and ntlm
web_set_sockets_option / web_set_max_html_param_lenMostly not needed; response size limits live in jmeter.propertiesNot needed
web_concurrent_start() … web_concurrent_end()Parallel Controller (plugin), or HTTP Request "Retrieve All Embedded Resources" with a parallel download count when the group is page assetshttp.batch([...]) — native
web_set_timeout()HTTP Request → Advanced → connect / response timeoutsparams.timeout
Run-time setting: download non-HTML resourcesHTTP Request → "Retrieve All Embedded Resources" (plus a URL-must-match filter)Not supported — k6 does not parse pages for assets. Request them explicitly, or accept that k6 measures the API and not the page
Run-time setting: simulate new user each iterationHTTP Cookie Manager → "Clear cookies each iteration" (and Cache Manager likewise)Default behaviour — k6 resets the per-VU cookie jar each iteration; noCookiesReset: true keeps it
Correlation — the part that is the actual work
LoadRunnerJMeterk6
web_reg_save_param("p", "LB=…", "RB=…")Boundary Extractor (left/right boundary) — child of the sampler; variable ${p}Slice res.body: const p = res.body.substring(res.body.indexOf(lb) + lb.length).split(rb)[0], or a regex
web_reg_save_param_regexp("RegExp=…")Regular Expression Extractor; template $1$; match no. 1const p = res.body.match(/…/)[1]
web_reg_save_param_json("QueryString=$.a.b")JSON Extractor (JSONPath) or JSON JMESPath Extractorres.json('a.b') — k6 supports a dotted selector natively
web_reg_save_param_xpath()XPath2 Extractor (needs the response to be XML/XHTML; tick "Use Tidy" for real-world HTML)parseHTML(res.body).find('css selector') — k6 has CSS selectors, not XPath
"ORD=ALL" → p_count, p_1, p_2…Match No. -1${p_matchNr}, ${p_1}, ${p_2}… — same shape[...res.body.matchAll(/…/g)].map(m => m[1]) — an array
"ORD=n" / lr_paramarr_randomMatch No. n; random: Match No. 0arr[n-1]; random: arr[Math.floor(Math.random()*arr.length)]
"NotFound=warning" / "NotFound=error"Default Value field (the variable is set to it when nothing matches); to fail, add a Response Assertion on the variableCheck the value: check(p, { 'found': v => v !== undefined }); a missing match does not fail the request by itself
"SaveLen", "SaveOffset"No field — use a regex that captures the exact spanString slicing
Automatic correlation rules (the VuGen library)Nothing built in. Every extractor is written by hand, once. That is the job the £250 review scopesNothing built in — same
Registration before the requestExtractor is a child of the request — it reads that sampler's responseCode after the call — reads res
Checks, transactions, timing
LoadRunnerJMeterk6
web_reg_find("Text=…")Response Assertion → Text Response → Containscheck(res, { 'has text': r => r.body.includes('…') })
web_reg_find("Fail=NotFound")Same — an assertion failing marks the sample failedA failed check does not fail the test; add a threshold on checks, or fail()
web_reg_find("SaveCount=n")Regex Extractor with Match No. -1, then assert on ${n_matchNr}res.body.match(/…/g).length
web_global_verification()Response Assertion at Thread Group / Test Plan scope — applies to every sampler below itA helper called after every request, or a threshold on a custom metric
HTTP status checks (implicit in LR)Implicit — a non-2xx/3xx is a failed sample unless "Ignore status" is setNot implicit — a 500 is a successful request in k6 unless you check(res, { 'status 200': r => r.status === 200 }). The most common migration surprise
lr_start_transaction("T") … lr_end_transaction("T", LR_AUTO)Transaction Controller "T" with "Generate parent sample" ticked — the transaction is one row in the reportgroup('T', () => { … }) — reported as group_duration, tagged by name
lr_end_transaction("T", LR_FAIL)Any failed sampler inside fails the transactionGroups do not fail; failures are per-check. Use a threshold: 'checks{group:::T}': ['rate>0.99'] — the group tag is the name with a :: prefix, so three colons in the selector
lr_think_time(n)Constant Timer (n ms) as a child of the next sampler, or Uniform Random Timer for a spreadsleep(n) — seconds
Run-time settings → Think time (multiply / random %)Uniform Random Timer, or ${__Random(…)} in a Constant Timersleep(Math.random() * 2 + 1)
Run-time settings → PacingConstant Throughput Timer (per thread), or Flow Control Action "pause" at the end of the loopAn arrival-rate executor is the right tool; otherwise sleep for the remainder of the interval
lr_rendezvous()Synchronizing Timer — holds threads until n arriveNo equivalent. Nothing in the options or scenarios reference synchronises VUs; the closest is a constant-vus executor starting everyone at once
Think time excluded from transaction time (Analysis option)Transaction Controller → untick "Include duration of timer and pre/post processors"group_duration is "the total time to execute the group function", so a sleep inside the group is counted; http_req_duration never includes it. Keep sleep outside the group
Parameters and data
LoadRunnerJMeterk6
.prm parameter from a .dat fileCSV Data Set Config — filename, variable names, delimiternew SharedArray('name', () => open('file.csv').split('\n').slice(1).map(…)) — loaded once, shared by all VUs
Select next row: SequentialSharing mode "Current thread" (each thread walks the file from the top)data[__ITER % data.length]
Select next row: UniqueSharing mode "All threads" — each thread takes the next unread linedata[(__VU - 1) % data.length] per VU, or exec.scenario.iterationInTest (from k6/execution) for a number unique across every VU and iteration in the run
Select next row: RandomSharing mode "Current thread" + ${__RandomFromMultipleVars}, or a Random CSV Data Set (plugin)data[Math.floor(Math.random() * data.length)]
Update value on: Each iteration / Each occurrence / OnceCSV Data Set advances once per iteration (when the element is reached); "Once" = read it in a setUp group into a propertyWhere you index it — top of the default function = per iteration; inside setup() = once
When out of values: Cycle / Abort VuserRecycle on EOF = True / False; Stop thread on EOFModulo for cycle; exec.test.abort() to stop
lr_eval_string("{p}")${p} anywhere in a fieldTemplate literal `…${p}…`
lr_save_string("v", "p")User Defined Variables (static), or vars.put("p", "v") in JSR223const p = 'v'
lr_save_int / lr_save_datetime${__intSum}, ${__time(yyyy-MM-dd)}Plain JS; new Date().toISOString()
Unique number / Vuser ID parameter${__threadNum}, ${__counter(FALSE)}, ${__UUID}__VU, __ITER, scenario.iterationInTest, uuidv4() from k6-utils
Date/time parameter with format and offset${__timeShift(yyyy-MM-dd,,P1D,,)}new Date(Date.now() + 86400000) and format by hand
Logic and code
LoadRunnerJMeterk6
if / else in CIf Controller (JavaScript or Groovy expression: ${__groovy(vars.get("p") == "x")})Plain JS
for / whileLoop Controller (fixed) / While Controller (condition)Plain JS
switch on a parameterSwitch Controller — by index or by namePlain JS
Custom C function, string handling (sprintf, strtok…)JSR223 Sampler / Pre / PostProcessor in Groovy — hand-ported, and priced separately in the reviewA JS function — hand-ported, same
lr_output_message / lr_error_messagelog.info(…) in JSR223; Debug Sampler for variablesconsole.log(…)
lr_exit(LR_EXIT_VUSER, LR_FAIL)Flow Control Action → Stop Thread; a failed assertion with "Stop thread on error" on the Thread Groupexec.test.abort() (whole test) or return from the iteration
lr_load_dll() / external C librariesDoes not map — rewrite in Groovy or call a Java libraryDoes not map — k6 has no FFI; xk6 extensions in Go if it truly must
Results and reports
  • Analysis sessionJMeter: jmeter -n -t plan.jmx -l r.jtl -e -o report/. k6: end-of-test summary, --out json, or handleSummary()
  • Transaction percentilesJMeter dashboard / Aggregate Report (p90/95/99). k6: http_req_duration p(95), group_duration
  • SLA rulesJMeter: none built in — a gate in CI (perf-reporting). k6: options.thresholds, and a breached threshold fails the run
  • Online monitorsJMeter: Backend Listener → InfluxDB/Grafana. k6: --out to Prometheus/InfluxDB/Grafana Cloud
  • Open a .jtl without the GUIThe free viewer at tools.martkos-it.co.uk/report — also reads k6 JSON
Does not carry across — say so before you start
Protocols other than Web – HTTP/HTML. TruClient and DevWeb are different scripting models, not different syntax. Citrix, SAP GUI, RTE, RDP, Oracle NCA and the like drive a client, not a protocol; JMeter and k6 have nothing to convert them to. IP spoofing — JMeter has a per-sampler source address, k6 has none. Network virtualisation and Diagnostics — neither. VTS (Virtual Table Server) — JMeter: the Inter-Thread Communication plugin or an external store; k6: an external store — k6/x/redis ships a Redis client, marked experimental. Automatic correlation — nothing in either tool finds session values for you; a recording replays the values it recorded and fails on the second run until every one has an extractor. That, not the syntax, is where the migration effort goes — and it is what the £250 review counts before anyone quotes you.

Convert a script — free, in your browser

JMeter, k6 and Gatling, any direction. Nothing is uploaded.

All 51 cheat sheets

Free to read and free to print — no signup. Want them as A4 PDFs you can print? Get all 51 as a PDF bundle for £14.99.
A Martkos IT reference sheet · Blog · Free tools