---
title: "Version 2.3.0 release notes | Grafana k6 documentation"
description: "The release notes for Grafana k6 version 2.3.0"
---

> For a curated documentation index, see [llms.txt](/llms.txt). For the complete documentation index, see [llms-full.txt](/llms-full.txt).

# Version 2.3.0 release notes

k6 `v2.3.0` is here 🎉! This release includes:

- A `--scenario` flag to run selected parts of a test without editing the script.
- A `--once` flag to reuse load-test scripts for smoke and functional testing.
- Experimental async `group()` support that keeps metrics in the right group across `await`.
- Built-in byte encoding, Set operations, and raw JSON values.
- Opt-in fetching of missing TLS certificates for servers that work in browsers but fail in k6.
- Static labels for Prometheus remote write, nanosecond log timestamps, and WebSocket ready-state constants.

## New features

### Run selected scenarios [#6360](https://github.com/grafana/k6/pull/6360)

When a script tests several services or user journeys, you may only need to run the part you are working on. The new `--scenario` flag lets you select named scenarios without editing the script, adding environment-variable selection code, or splitting it into separate scripts. This addresses the long-standing [request to run a subset of scenarios](https://github.com/grafana/k6/issues/2780).

For example, this script defines two workloads:

js ![Copy code to clipboard](/media/images/icons/icon-copy-small-2.svg) Copy

```js
import http from 'k6/http';

export const options = {
  scenarios: {
    homepage: {
      executor: 'shared-iterations',
      exec: 'homepage',
      vus: 2,
      iterations: 10,
    },
    contacts: {
      executor: 'shared-iterations',
      exec: 'contacts',
      vus: 1,
      iterations: 5,
    },
  },
};

export function homepage() {
  http.get('https://test.k6.io/');
}

export function contacts() {
  http.get('https://test.k6.io/contacts.php');
}
```

Run only the homepage workload, keeping its two VUs and ten total iterations:

shell ![Copy code to clipboard](/media/images/icons/icon-copy-small-2.svg) Copy

```shell
k6 run --scenario homepage script.js
```

Select multiple scenarios with comma-separated names:

shell ![Copy code to clipboard](/media/images/icons/icon-copy-small-2.svg) Copy

```shell
k6 run --scenario homepage,contacts script.js
```

Each selected scenario keeps its executor, load, timing, function, environment, tags, and browser options. Without the flag, k6 runs all configured scenarios. Selection also works with `k6 cloud run` and `k6 archive`; archives retain the selected configuration.

Names must match configured scenarios. Load shortcuts such as `--vus` and `--duration` cannot be combined with selection; set the workload in each scenario instead. k6 warns and skips thresholds tagged for configured scenarios you exclude, while keeping global thresholds and other filters. See the [scenario documentation](/docs/k6/v2.3.x/using-k6/scenarios/#run-selected-scenarios) for details.

### Run a script once with `--once` [#6338](https://github.com/grafana/k6/pull/6338)

The `--once` flag is now the recommended way to run a script once, for both protocol and browser tests, while preserving the scenario’s function and browser configuration. It supports scripts with at most one scenario and works with `k6 run`, `k6 cloud run`, and `k6 archive`. For example, this configuration runs `checkout()` repeatedly with ten VUs for 30 seconds:

js ![Copy code to clipboard](/media/images/icons/icon-copy-small-2.svg) Copy

```js
export const options = {
  scenarios: {
    checkout: {
      executor: 'constant-vus',
      exec: 'checkout',
      vus: 10,
      duration: '30s',
    },
  },
};
// The rest of the script defines checkout().
```

shell ![Copy code to clipboard](/media/images/icons/icon-copy-small-2.svg) Copy

```shell
k6 run --once script.js
```

With `--once`, k6 changes this scenario to `shared-iterations` with one VU and one iteration, calling `checkout()` once instead of repeatedly for 30 seconds. Previously, shortcuts such as `--vus 1 --iterations 1` replaced the script’s scenarios with a default scenario, discarding settings such as the selected function and browser options. This broke browser scripts because the configuration needed to launch Chromium was missing; `--once` keeps that configuration. To combine scenario selection with `--once`, see [Run each selected scenario once](/docs/k6/v2.3.x/using-k6/scenarios/#run-each-selected-scenario-once).

### Keep metrics grouped across asynchronous calls [#6340](https://github.com/grafana/k6/pull/6340), [#6341](https://github.com/grafana/k6/pull/6341), [#6342](https://github.com/grafana/k6/pull/6342), [#6343](https://github.com/grafana/k6/pull/6343), [#6344](https://github.com/grafana/k6/pull/6344)

You can now use an async function in `group()` by enabling the experimental `async-metric-context` feature. Requests and checks after an `await` keep their group, and `group_duration` measures until the callback’s returned promise settles. Previously, `group()` rejected async functions, and promise callbacks could lose the group tag.

js ![Copy code to clipboard](/media/images/icons/icon-copy-small-2.svg) Copy

```js
import { check, group } from 'k6';
import http from 'k6/http';

export default async function () {
  await group('browse', async () => {
    const response = await http.asyncRequest('GET', 'https://test.k6.io/');
    check(response, { 'page loaded': (r) => r.status === 200 });
    await http.asyncRequest('GET', 'https://test.k6.io/contacts.php');
  });
}
```

shell ![Copy code to clipboard](/media/images/icons/icon-copy-small-2.svg) Copy

```shell
k6 run --features async-metric-context script.js
```

Both requests and the check belong to `browse`, including the work after the first `await`. The feature also preserves custom tags and metadata across promises, timer callbacks, `k6/websockets` listeners, and gRPC stream listeners. Changes inside a group or callback stay local to that work and its asynchronous descendants instead of leaking into unrelated work.

### Fetch missing TLS certificates [#6137](https://github.com/grafana/k6/pull/6137)

Some HTTPS servers work in browsers but fail in k6 because they omit an intermediate certificate. The new `tlsAIAFetch` option lets k6 fetch that missing certificate while still verifying the server’s identity:

js ![Copy code to clipboard](/media/images/icons/icon-copy-small-2.svg) Copy

```js
export const options = {
  tlsAIAFetch: true,
};
```

This is opt-in and works with HTTP and gRPC connections. Thanks, @vtorosyan!

### Static labels for Prometheus remote write [#6071](https://github.com/grafana/k6/pull/6071)

When several k6 instances send metrics to the same Prometheus server, labels let you tell their results apart. Use `K6_PROMETHEUS_RW_LABELS` to identify the job, environment, or server on every time series sent by that output:

shell ![Copy code to clipboard](/media/images/icons/icon-copy-small-2.svg) Copy

```shell
K6_PROMETHEUS_RW_LABELS="environment=production,server=srv1" \
k6 run --out experimental-prometheus-rw script.js
```

Thanks, @rohan-patnaik!

### Nanosecond log timestamps [#6310](https://github.com/grafana/k6/pull/6310)

Logs with second-precision timestamps can lose their order when a log service sorts messages emitted within the same second. Enable nanosecond timestamps with `--log-ns-timestamps` to make those messages easier to order and correlate:

shell ![Copy code to clipboard](/media/images/icons/icon-copy-small-2.svg) Copy

```shell
k6 --log-ns-timestamps --log-format=json run script.js
```

This also works with plain-text logs when `--no-color` is set.

### WebSocket ready-state constants [#6306](https://github.com/grafana/k6/pull/6306)

The `WebSocket` constructor and its instances now expose the same connection-state constants as browsers: `CONNECTING`, `OPEN`, `CLOSING`, and `CLOSED`. For an existing socket, you can check its state before sending a message:

js ![Copy code to clipboard](/media/images/icons/icon-copy-small-2.svg) Copy

```js
if (socket.readyState === WebSocket.OPEN) {
  socket.send('hello');
}
```

### Encode bytes and compare sets with JavaScript built-ins [#6382](https://github.com/grafana/k6/pull/6382)

You can now convert `Uint8Array` data to and from hex or base64 with built-in methods, and compare sets with operations such as `difference()`, `intersection()`, and `union()`. Use them to prepare binary test data or check which fields are missing from a response.

JavaScript ![Copy code to clipboard](/media/images/icons/icon-copy-small-2.svg) Copy

```javascript
const bytes = Uint8Array.fromHex('6b36');
console.log(bytes.toBase64()); // azY=

const expected = new Set(['id', 'name', 'email']);
const received = new Set(['id', 'name']);
console.log([...expected.difference(received)]); // ["email"]
```

The Sobek update also adds `Error.isError()`, `JSON.rawJSON()`, and `JSON.isRawJSON()`. For example, `JSON.rawJSON()` lets you include an exact numeric value in a JSON payload without rounding it to a JavaScript `Number` first:

JavaScript ![Copy code to clipboard](/media/images/icons/icon-copy-small-2.svg) Copy

```javascript
JSON.stringify({ id: JSON.rawJSON('9007199254740993') });
// '{"id":9007199254740993}'
```

Thanks to @arukiidou and @xtrafrancyz for the upstream implementations!

### Read the execution result before k6 exits [#6388](https://github.com/grafana/k6/pull/6388)

When k6 stays alive with `--linger`, tools monitoring the process cannot use its exit status to tell whether the test finished successfully or aborted. The REST API’s `/v1/status` response now includes `execution_result`, with the test’s exit code once it is known. Before then, the field is `null`. Thanks, @yorugac!

For example, query a lingering process after a script calls `exec.test.abort()`:

shell ![Copy code to clipboard](/media/images/icons/icon-copy-small-2.svg) Copy

```shell
curl -s http://localhost:6565/v1/status | jq '.data.attributes.execution_result'
```

JSON ![Copy code to clipboard](/media/images/icons/icon-copy-small-2.svg) Copy

```json
{
  "exit_code": 108
}
```

## UX improvements and enhancements

- [#6408](https://github.com/grafana/k6/pull/6408) Explains how to enable experimental async support when `group()` rejects an async callback.
- [#6402](https://github.com/grafana/k6/pull/6402) Fixes a missing space in the `--no-usage-report` help text. Thanks, @yats0x7!
- [#6339](https://github.com/grafana/k6/pull/6339) Adds login and token-configuration guidance when Grafana Cloud commands return an authentication error. Thanks, @Swapnil-Biswas!

## Bug fixes

- [#6414](https://github.com/grafana/k6/pull/6414) Stops screenshot capture from waiting until the page closes when a browser command does not respond, so scripts can catch the timeout and continue cleanup.
- [#6412](https://github.com/grafana/k6/pull/6412) Lets waits for hidden or detached browser elements finish when the element is already absent, instead of timing out or throwing an error.
- [#6368](https://github.com/grafana/k6/pull/6368) Makes `crypto.getRandomValues()` throw a catchable `TypeError` instead of crashing k6 when called without an argument or with a typed array whose length was overridden to a negative value. Thanks, @hyuraku!
- [#6327](https://github.com/grafana/k6/pull/6327) Fixes a data race when closing a browser context while other browser operations access it. Thanks, @JohnPei1!
- [#6351](https://github.com/grafana/k6/pull/6351) Prevents clearing an expired timer from running a later timeout or interval too early, and fixes VUs hanging when a `k6/websockets` connection is closed before its handshake finishes.
- [#6349](https://github.com/grafana/k6/pull/6349) Fixes inflated `http_req_sending` and `http_req_duration` values when making HTTPS requests through an HTTPS proxy. Thanks, @kausthubhk!
- [#5922](https://github.com/grafana/k6/pull/5922) Stops the test with an error when a ramping-VU scenario cannot start a VU, instead of silently stopping the scenario and reporting success. Thanks, @HwangRock!
- [#6163](https://github.com/grafana/k6/pull/6163) Preserves Web Vitals from intermediate pages when a browser test navigates several times in the same tab, so results include those pages as well as the last one.
- [#6355](https://github.com/grafana/k6/pull/6355) Prevents a response-body leak when reading a digest authentication challenge fails. Thanks, @cuishuang!
- [#5949](https://github.com/grafana/k6/pull/5949) Supports sending metrics to InfluxDB behind a reverse proxy with a URL path prefix, such as `https://host/influxdb/database`. Thanks, @o6ivp!
- [#6235](https://github.com/grafana/k6/pull/6235) Uses forward slashes in remote screenshot paths on Windows. Thanks, @adarshsm!
- [#6238](https://github.com/grafana/k6/pull/6238) Sends `null` and `undefined` form fields as empty values instead of the string `<nil>`, and warns when a nested object cannot be encoded as a form field. Thanks, @djedi-knight!
- [#6279](https://github.com/grafana/k6/pull/6279) Fixes browser tests stalling when VUs share a remote Chrome instance, so pages can run concurrently.
- [#6298](https://github.com/grafana/k6/pull/6298) Preserves browser trace spans that were lost when a test ended. Thanks, @mem!
- [#6385](https://github.com/grafana/k6/pull/6385) Fixes an integer overflow that prevented compilation on 32-bit ARM and x86 systems. Thanks, @GourangaDasSamrat!

## Maintenance and internal improvements

- [#6413](https://github.com/grafana/k6/pull/6413) Updates the browser role-selector test fixture for Chromium’s image-map rendering.
- [#6369](https://github.com/grafana/k6/pull/6369), [#6370](https://github.com/grafana/k6/pull/6370) Adds the binary’s build origin and a locally stored random installation ID to usage reports, helping distinguish build sources and measure active installations. Both respect `--no-usage-report`.
- [#6209](https://github.com/grafana/k6/pull/6209) Simplifies the internal handling of Cloud commands. Thanks, @moko-poi!
- [#6236](https://github.com/grafana/k6/pull/6236) Adds a smoke test before publishing browser Docker images to catch missing or broken Chromium installations.
- [#6245](https://github.com/grafana/k6/pull/6245) Enables CI on the v1 maintenance branch.
- [#6246](https://github.com/grafana/k6/pull/6246), [#6277](https://github.com/grafana/k6/pull/6277) Automates approvals and merging for eligible dependency updates.
- [#6256](https://github.com/grafana/k6/pull/6256), [#6271](https://github.com/grafana/k6/pull/6271) Improves contributor acknowledgments and the release checklist.
- [#6270](https://github.com/grafana/k6/pull/6270) Restores Debian package publishing after `bzip2` was removed from the packaging base image.
- [#6301](https://github.com/grafana/k6/pull/6301), [#6373](https://github.com/grafana/k6/pull/6373) Corrects the TC39 test-package path and built-in module locations in contributor documentation. Thanks, @umekikazuya and @ashrafiucse!
- [#6312](https://github.com/grafana/k6/pull/6312), [#6380](https://github.com/grafana/k6/pull/6380), [#6394](https://github.com/grafana/k6/pull/6394), [#6398](https://github.com/grafana/k6/pull/6398) Uses shared CI workflows, lint configuration, and Go test versions, and fixes findings from the newer linter.
- [#6409](https://github.com/grafana/k6/pull/6409) Updates Test262 expectations for Unicode tests that pass with the newer Go version.
- [#6325](https://github.com/grafana/k6/pull/6325) Moves browser option parsing into the JavaScript mapping layer without changing script behavior. Thanks, @Shobhit-Nagpal!
- [#6265](https://github.com/grafana/k6/pull/6265), [#6266](https://github.com/grafana/k6/pull/6266), [#6267](https://github.com/grafana/k6/pull/6267), [#6303](https://github.com/grafana/k6/pull/6303), [#6304](https://github.com/grafana/k6/pull/6304), [#6331](https://github.com/grafana/k6/pull/6331), [#6332](https://github.com/grafana/k6/pull/6332), [#6357](https://github.com/grafana/k6/pull/6357), [#6358](https://github.com/grafana/k6/pull/6358), [#6363](https://github.com/grafana/k6/pull/6363), [#6367](https://github.com/grafana/k6/pull/6367), [#6383](https://github.com/grafana/k6/pull/6383), [#6376](https://github.com/grafana/k6/pull/6376), [#6478](https://github.com/grafana/k6/pull/6478) Updates the Docker build image to Go 1.27.1, raises the module’s minimum Go version to 1.26.0, and updates gRPC to v1.84.0 (the example server uses v1.83.2), OpenTelemetry to v1.46.0, Brotli to v1.2.3, `klauspost/compress` to v1.20.0, esbuild to v0.28.2, Testify to v1.12.1, Protobuf to v1.36.12, `golang.org/x/crypto` to v0.56.0, and related Go dependencies.

## External contributors

Thanks to @adarshsm, @arukiidou, @ashrafiucse, @cuishuang, @djedi-knight, @GourangaDasSamrat, @HwangRock, @hyuraku, @JohnPei1, @kausthubhk, @mem, @moko-poi, @o6ivp, @rohan-patnaik, @Shobhit-Nagpal, @Swapnil-Biswas, @umekikazuya, @vtorosyan, @xtrafrancyz, @yats0x7, and @yorugac for their contributions.
