release notes/v2.2.0.md
k6 v2.2.0 is here 🎉! This release includes:
k6 cloud run --local-execution now streams k6's logs to Grafana Cloud, so the test run's log view works for local execution too.chromium.connectOverCDP(), which connects browser tests to an already-running Chromium instance.TextEncoder and TextDecoder available as globals, and WritableStream support in k6/experimental/streams.k6 cloud load-zone list command.merge-run-tags and freeze-env.There are no breaking changes in this release.
k6 cloud run --local-execution streams logs to Grafana Cloud #6171When a cloud test runs locally with k6 cloud run --local-execution, k6's logs now stream to the Grafana Cloud test run, so the run's log view is populated the same way it is for cloud execution. Previously, local-execution logs stayed on the machine running k6 and never reached the cloud. Work with Grafana's secrets management to safely work with secrets and redact them if they're accidentally leaked into logs are pushed. Use the new --no-cloud-logs flag opts out to opt out of streaming of logs when working with --local-execution:
k6 cloud run --local-execution script.js
k6 cloud run --local-execution --no-cloud-logs script.js
chromium.connectOverCDP() #6165The browser module can now attach to an existing Chromium-based browser over the Chrome DevTools Protocol, mirroring Playwright's browserType.connectOverCDP(). Pass the browser's WebSocket endpoint and k6 manages the returned browser's connection — it's auto-closed at the end of the iteration, though you can call close() earlier to release the connection on demand.
import { chromium } from 'k6/browser';
export default async function () {
const browser = await chromium.connectOverCDP('ws://localhost:9222/devtools/browser/<id>');
const page = await browser.newPage();
try {
await page.goto('https://quickpizza.grafana.com/');
} finally {
await page.close();
await browser.close();
}
}
Unlike the K6_BROWSER_WS_URL environment variable, the endpoint is a runtime value — you can, for example, request a fresh session URL from a browser provider's API in setup() and connect to it from the iterations.
TextEncoder and TextDecoder globals #6182TextEncoder and TextDecoder are now available as standard globals in both the init and VU contexts, no import required — matching how they are exposed in browsers and other JavaScript runtimes.
const encoded = new TextEncoder().encode('Hello, world!');
const decoded = new TextDecoder().decode(encoded);
WritableStream in k6/experimental/streams #6132The experimental streams module now implements WritableStream and WritableStreamDefaultWriter following the WHATWG Streams specification, complementing the existing ReadableStream and paving the way for a future TransformStream implementation.
import { WritableStream } from 'k6/experimental/streams';
export default async function () {
const stream = new WritableStream({
write(chunk) {
console.log(`wrote ${chunk}`);
},
});
const writer = stream.getWriter();
await writer.write('hello');
await writer.close();
}
k6 cloud load-zone list command #6142A new k6 cloud load-zone list subcommand lists the load zones — public and private — available in the configured Grafana Cloud k6 stack, mirroring the existing k6 cloud project list command. Output defaults to a human-readable table; pass --json to emit a JSON array instead.
$ k6 cloud load-zone list
Load zones for https://example.grafana.net:
ID NAME TYPE AVAILABLE
amazon:us:ashburn Ashburn, US (Amazon) public yes
amazon:sa:cape town Cape Town, SA (Amazon) public yes
handleSummary() timeout #5854The time budget for the handleSummary() callback — previously hardcoded to 120 seconds — is now configurable through the handleSummaryTimeout option or the K6_HANDLE_SUMMARY_TIMEOUT environment variable, so long-running tests with heavy summaries no longer fail with handleSummary() execution timed out. Thanks, @LBaronceli!
export const options = {
handleSummaryTimeout: '5m',
};
merge-run-tags and freeze-envTwo new experimental flags join the feature-flag system introduced in v2.1.0:
merge-run-tags merges run tags per key across config layers, so options.tags in a script is no longer silently discarded when --tag or K6_TAGS is also used — higher-priority layers win on conflicting keys instead of replacing the whole map. Thanks, @yordis!freeze-env freezes the __ENV object, so modifications from script code throw a TypeError (in strict mode) instead of silently persisting across iterations and scenarios. Thanks, @lohitkolluri!k6 run --features merge-run-tags,freeze-env script.js
response.allHeaders(), headerValue(), headerValues(), and headersArray() — return the raw wire headers (including Set-Cookie and security-related headers), correctly paired with each hop of a redirect chain instead of Chrome's provisional headers. As part of this, headerValues() now matches header names case-insensitively and splits repeated values on newlines rather than commas, and the browser_data_sent/browser_data_received metrics now include the raw header bytes and no longer vary run-to-run with CDP event ordering.k6 cloud reject the run flags (for example, --vus) with an unknown flag error and a non-zero exit code. Previously k6 cloud --vus 10 script.js accepted the flags, printed the help text, and exited 0 — running tests with k6 cloud directly was deprecated in v2.0.0 in favor of k6 cloud run.K6_CLOUD_SECRETS_TOKEN and K6_CLOUD_SECRETS_ENDPOINT when a test run is reused via K6_CLOUD_PUSH_REF_ID under --local-execution, instead of suggesting the --local-execution flag the user is already using.catch blocks to the browser examples so a failing iteration reports the original error instead of a subsequent page.close() failure. Thanks, @locker95!error_code metric tag stays correct when k6 is built with Go 1.27 (whose x/net/http2 delegates to the standard library), and explicitly enables HTTP/2 negotiation on VU transports.--out loki and cloud log streaming no longer lose the final batch, and emits a k6 dropped N log messages warning when the cloud log buffer overflows instead of dropping logs silently.fs.open() calls on the same file no longer read zero or truncated bytes.Sec-WebSocket-Protocol header when tailing Grafana Cloud logs; spec-strict servers rejected the handshake with websocket: bad handshake.rate unset when the observed duration is zero, instead of computing +Inf and spuriously failing rate thresholds. Thanks, @samarth70!max=0. Thanks, @Solaris-star!K6_OTEL_HEADERS. Thanks, @lukdz!SharedArray deep-freezing JS primitives, which needlessly wrapped large strings in String objects — cutting memory usage in the reported reproduction from roughly 1 GB to 100 MB.k6/x/ modules, output extensions selected with --out, and k6 x subcommands). Private and unlisted extensions are never reported, and the existing --no-usage-report opt-out covers it.WeakMap/WeakSet entries garbage-collectable, improving string and typed-array correctness and performance, and bounding regular-expression backtracking memory.k6 cloud run --local-execution from the legacy v1 cloud API to the v6 and provisioning APIs, and quietens its status polling logs. User-facing behavior is unchanged, and k6 run --out cloud stays on the legacy API.k6 cloud run --local-execution via the K6_CLOUD_METRICS_PUSH_URL and K6_CLOUD_TEST_RUN_TOKEN environment variables.github.com/grafana/k6-cloud-openapi-client-go, consuming the upstream retry body-reset fix (dropping the k6-side workaround) and the int64 resource-ID widening.go.k6.io/k6/v2 module move..github/go-versions.env.get-vault-secrets v2.google.golang.org/grpc to v1.83.0 [security].golang.org/x/net to v0.56.0 and golang.org/x/text to v0.39.0 in the gRPC server example [security].golang.org/x packages, klauspost/compress, mattn/go-isatty, mccutchen/go-httpbin, the OpenTelemetry and Prometheus protobufs, andybalholm/brotli, and evanw/esbuild.1.26.5, Alpine to 3.24.1, Debian to trixie-20260623).actions/checkout to v7, golangci/golangci-lint-action to v9.3.0, and the grafana/shared-workflows actions.A huge thank you to the external contributors who helped during this release: @LBaronceli, @yordis, @lohitkolluri, @locker95, @rohan-patnaik, @somak2kai, @samarth70, @Solaris-star, @lukdz, @the-onewho-knocks, @hyuraku, and @Martonveghcode! 🙏