CheckOps home

Guide

k6 2.0 Migration Guide: Upgrade From k6 1.x

A step-by-step k6 2.0 migration guide: removed commands and flags, the externally-controlled executor, exit code 97, options.cloud, OTel and FID to INP.

15 May 2026 · 11 min · CheckOps

Grafana Labs released k6 v2.0.0 on 11 May 2026, about two weeks after the first release candidate on 28 April. It is a clean-up release. The notes describe it as “completing the cleanup of deprecated APIs, old commands, and obsolete configuration options”. There is no big new scripting model to learn, but there are enough removals that an unplanned upgrade can break a CI pipeline.

This k6 2.0 migration guide walks through every breaking change that affects test authors and pipeline owners, in the order I would tackle them, with before and after examples. It is a practical companion to our k6 1.0 essay from last year. If you are choosing a load testing tool rather than upgrading one, see the best performance testing tools of 2025.

The source for everything here is the official k6 v2.0.0 release notes and Grafana’s Migrate to k6 v2 page. Check them for your exact version before you upgrade.

k6 2.0 at a glance

Change Who is affected What to do
k6 pause, resume, scale, status removed Anyone scripting runtime control No replacement; remove usage
externally-controlled executor removed Scripts using it Switch to another executor
k6 login removed Cloud and InfluxDB users k6 cloud login; K6_INFLUXDB_* env vars
k6 cloud script.js removed Cloud users k6 cloud run script.js
--upload-only removed Cloud users k6 cloud upload script.js
--no-summary removed CI pipelines --summary-mode=disabled
--summary-mode=legacy removed CI parsing summaries Use compact or full
options.ext.loadimpact removed Older scripts Move to options.cloud
Stack required for k6 cloud Cloud users k6 cloud login, K6_CLOUD_STACK_ID or stackID
Cloud non-threshold aborts exit 97 CI pipelines Update exit-code handling
k6/experimental/redis removed Redis tests Import k6/x/redis
OTel exporterType removed OTel output users K6_OTEL_EXPORTER_PROTOCOL
browser_web_vital_fid removed Browser tests, dashboards Use browser_web_vital_inp
HTTP API server off by default REST API users --address=localhost:6565
Go module path is go.k6.io/k6/v2 Extension authors Update imports

Step 1: inventory how you use k6

Before you upgrade anything, search your repositories and CI configuration for the strings that 2.0 removes. A quick grep catches most of them:

grep -rnE "externally-controlled|k6 (pause|resume|scale|status|login)|--no-summary|--upload-only|summary-mode=legacy|loadimpact|experimental/redis|K6_OTEL_EXPORTER_TYPE|K6_OTEL_SINGLE_COUNTER_FOR_RATE|K6_BINARY_PROVISIONING|K6_ENABLE_COMMUNITY_EXTENSIONS|web_vital_fid" .

Also search for k6 cloud followed by a file name, which is the old positional form, and any reference to port 6565.

If nothing matches, your upgrade may be as simple as changing the version. If several things match, work through the steps below.

Step 2: fix CLI commands and flags

Removed runtime control commands

k6 pause, k6 resume, k6 scale and k6 status are gone. The release notes say they “have not been functional for most use cases” because the REST API they relied on was limited to specific execution modes. There is no replacement. If you scripted any of these, remove them and design the load profile up front instead.

k6 login becomes k6 cloud login

The top-level k6 login command and its subcommands are removed.

# Before
k6 login cloud

# After
k6 cloud login

InfluxDB authentication is no longer configured through a login command. Use the K6_INFLUXDB_* environment variables instead.

Cloud commands

# Before
k6 cloud script.js
k6 cloud --upload-only script.js

# After
k6 cloud run script.js
k6 cloud upload script.js

Summary flags

# Before
k6 run --no-summary script.js
k6 run --summary-mode=legacy script.js

# After
k6 run --summary-mode=disabled script.js
k6 run --summary-mode=compact script.js   # the default
k6 run --summary-mode=full script.js

There is no direct equivalent of the legacy summary. If a CI step parses the end-of-test summary text, it will break. This is a good moment to stop parsing console output and use a machine-readable output or handleSummary() instead.

Step 3: replace the externally-controlled executor

The externally-controlled executor let external systems change virtual users and pause or resume a running test. The notes describe it as “legacy code from an older k6 Cloud architecture”. In 2.0, “any test script with executor: externally-controlled will fail to start.”

Pick an executor that describes the load profile you actually want:

// Before
export const options = {
  scenarios: {
    main: { executor: 'externally-controlled', vus: 10, maxVUs: 50, duration: '10m' },
  },
};

// After: ramp virtual users in stages
export const options = {
  scenarios: {
    main: {
      executor: 'ramping-vus',
      startVUs: 10,
      stages: [
        { duration: '3m', target: 50 },
        { duration: '5m', target: 50 },
        { duration: '2m', target: 0 },
      ],
    },
  },
};

If you care about requests per second rather than concurrent users, constant-arrival-rate or ramping-arrival-rate is usually a better model. The release notes list ramping-vus, constant-vus and constant-arrival-rate as examples.

Step 4: move cloud configuration to options.cloud

options.ext.loadimpact is no longer supported. The fix is mechanical:

// Before
export const options = {
  ext: { loadimpact: { projectID: 12345, name: 'Checkout load test' } },
};

// After
export const options = {
  cloud: { projectID: 12345, name: 'Checkout load test' },
};

A stack is now required

All k6 cloud commands now require a stack. Previously k6 fell back to the first available stack with a warning; that fallback is gone. k6 cloud login now asks for both a token and a stack. In CI, set K6_CLOUD_STACK_ID or the stackID script option.

k6 also no longer migrates the old pre-1.0 config file path {USER_CONFIG_DIR}/loadimpact/config.json. Move it to {USER_CONFIG_DIR}/k6/config.json or rerun k6 cloud login.

Step 5: update CI exit-code handling

This is the change most likely to surprise pipeline owners, and it is a good one. Previously most cloud aborts returned exit code 0, which made a failed run look successful.

Scenario Before After
Finished 0 0
Aborted by threshold 99 99
Aborted by system, limit, script error, user or timeout 0 97

If your pipeline treats any non-zero exit as failure, you are already fine, and you will now correctly catch aborted runs. If it has special cases, for example “allow 99 but fail on everything else”, check how it handles 97. A simple wrapper might look like this:

k6 cloud run script.js
code=$?
if [ "$code" -eq 99 ]; then echo "Thresholds failed"; exit 1; fi
if [ "$code" -eq 97 ]; then echo "Cloud run aborted"; exit 1; fi
exit $code

Step 6: update modules and extensions

Redis

k6/experimental/redis is removed from the core binary. Change the import to k6/x/redis. With auto-extension resolution, k6 provisions the xk6-redis extension automatically when it sees that import.

// Before
import redis from 'k6/experimental/redis';

// After
import redis from 'k6/x/redis';

Environment variables for extensions

Remove K6_BINARY_PROVISIONING and K6_ENABLE_COMMUNITY_EXTENSIONS. Auto-extension resolution is enabled by default, and community extensions are resolved through the default build service.

For extension authors: the Go module path

The module path is now go.k6.io/k6/v2, following Go’s major-version conventions. For most extensions this is a find-and-replace:

// Before
import "go.k6.io/k6/js/modules"

// After
import "go.k6.io/k6/v2/js/modules"

k6 also dropped easyjson in favour of the standard library’s encoding/json. Extension authors who relied on generated marshalling methods on k6 types need to remove those dependencies.

Step 7: update observability outputs

OpenTelemetry

Replace K6_OTEL_EXPORTER_TYPE with K6_OTEL_EXPORTER_PROTOCOL, which accepts grpc and http/protobuf. Remove K6_OTEL_SINGLE_COUNTER_FOR_RATE. Rate metrics are now always exported as a single counter with a condition attribute (nonzero or zero), so dashboards built on the old pair of counters need updating. If you send k6 data to OpenTelemetry collectors and Grafana, check those queries before you upgrade production pipelines.

Browser web vitals: FID is gone

The embedded web-vitals library moved from v3 to v5.1.0, and browser_web_vital_fid was removed. First Input Delay was replaced as a Core Web Vital by Interaction to Next Paint. Update thresholds, alerts and dashboards:

// Before
thresholds: { browser_web_vital_fid: ['p(75) < 100'] },

// After
thresholds: { browser_web_vital_inp: ['p(75) < 200'] },

Treat the threshold values above as placeholders. INP and FID measure different things, so set new budgets from your own baseline data rather than copying old FID numbers.

The REST API server

The k6 HTTP API server no longer starts by default. Previously it listened on localhost:6565. If anything talks to it, pass --address=localhost:6565 or set K6_ADDRESS.

Pinning k6 in CI during the migration

Most upgrade incidents happen because a pipeline pulls “latest” and quietly jumps a major version. Before you start, make the version explicit everywhere k6 runs.

  • Docker images. If you run the official grafana/k6 image, use an exact version tag rather than latest, and bump it deliberately in a pull request.
  • Package managers. Homebrew, apt and Chocolatey installs on self-hosted runners will upgrade with the rest of the machine. Pin the package or install a specific release binary from GitHub.
  • Custom binaries with extensions. If you build k6 with xk6, the extension modules must target the v2 module path before the build will work. Upgrade the extensions first, then k6.

A pinned version also makes the comparison in the rollout plan honest. You want to know that a change in results came from k6 2.0, not from an unrelated runner image update on the same day.

Verify your load tests still test something

A major-version upgrade is a good moment to check that your load tests would actually fail when they should. It is surprisingly common for thresholds to drift into meaninglessness over time.

  1. Break a threshold on purpose. Temporarily set an impossible latency threshold and confirm the run exits with 99 and the pipeline goes red.
  2. Break the script on purpose. Introduce a deliberate error, such as a bad import, and confirm the pipeline fails rather than reporting success. For cloud runs, this is where the new exit code 97 should appear.
  3. Check checks. Make sure check() calls are tied to a threshold on the checks rate. Checks alone do not fail a test.
  4. Compare a baseline. Run the same scenario on 1.x and 2.0 against the same environment and compare p95 latency, error rate and throughput. Small differences are normal; large ones deserve investigation before you blame the application.

This takes less than an hour and protects you from the worst kind of performance test: one that always passes.

Should you stay on k6 1.x for now?

Staying on a pinned 1.x release for a short time is reasonable if you depend on an extension that has not moved to the v2 module path yet, or if you are in the middle of a release freeze. It is not a good long-term plan. New fixes and features land on the 2.x line, and the longer you wait, the more scripts and dashboards accumulate the old patterns.

A practical rule: if the step 1 grep finds nothing and you use no custom extensions, upgrade this week. If it finds a handful of matches, schedule the fixes in your next sprint. If you depend on third-party extensions, check each one’s repository for v2 support and upgrade once they are ready.

What’s new in k6 2.0 worth using

It is not all removals. A few additions are useful straight away:

  • Cloud secrets in local execution. k6 cloud run --local-execution now makes Grafana Cloud secrets available through secrets.get() by default; pass --no-cloud-secrets to opt out.
  • k6 cloud project list lists projects, with --format=json for scripts.
  • Browser locator retries. newAction-based Locator APIs in the browser module now retry automatically.
  • Better warnings. k6 now warns when http.get() or http.head() receive extra arguments that are silently ignored.
  • Clearer cloud auth errors, telling you whether the token or the stack ID is missing.

A safe rollout plan

  1. Pin your current version in CI so nothing upgrades by accident.
  2. Run the grep from step 1 and open one pull request per affected repository.
  3. Upgrade a single non-critical pipeline to 2.0 and run your smoke load tests.
  4. Compare results with the 1.x baseline. The summary format will look different, so compare metrics, not text.
  5. Update dashboards and alerts for FID, OTel rate counters and any parsing of summaries.
  6. Roll out to the remaining pipelines, then remove the pin.

Pros and cons of upgrading now

Pros

  • Cloud aborts are no longer reported as success
  • Cleaner CLI, with fewer legacy paths to confuse new team members
  • Modern web-vitals metrics in browser tests
  • New features such as cloud secrets in local execution

Cons

  • Pipelines that parse legacy summaries or depend on removed commands will break
  • Extensions must be updated to the v2 module path before they work
  • Dashboards built on FID or old OTel counters need changes

What this means for your team

k6 2.0 is a maintenance release with real consequences for CI. Most script authors will change a few lines. Pipeline owners need to look closely at exit codes, summary parsing and cloud authentication. Extension authors have a mechanical but mandatory import change.

Plan an afternoon, not a sprint. Grep, fix, run a baseline, compare, and roll out. And take the exit code 97 change as a reminder: a load test that fails silently is worse than no load test at all.

Frequently asked questions

When was k6 2.0 released?

k6 v2.0.0 was released on 11 May 2026. The first release candidate, v2.0.0-rc1, was published on 28 April 2026.

Do I need to rewrite my k6 scripts for 2.0?

Usually not. Most scripts run unchanged, so the bigger work is often in CI scripts, Docker images and pipeline flags rather than in test code. You need changes if you use the externally-controlled executor, options.ext.loadimpact, k6/experimental/redis, or browser FID metrics.

What does exit code 97 mean in k6 2.0?

It means a cloud run was aborted for a reason other than a threshold failure, such as a system limit, script error, user abort or timeout. Previously these returned 0.

What replaces --no-summary in k6 2.0?

Use --summary-mode=disabled. The legacy summary mode was also removed; choose compact (the default) or full.

What should extension authors change for k6 2.0?

Update imports from go.k6.io/k6 to go.k6.io/k6/v2, and remove any reliance on easyjson-generated methods on k6 types.

Is the k6 REST API still available in 2.0?

Yes, but it no longer starts by default. Pass --address=localhost:6565, or set K6_ADDRESS, if a tool or script talks to it. The commands that used it to pause, resume or scale a running test were removed along with the externally-controlled executor.

Further reading on CheckOps