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/k6image, use an exact version tag rather thanlatest, 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.
- Break a threshold on purpose. Temporarily set an impossible latency threshold and confirm the run exits with 99 and the pipeline goes red.
- 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.
- Check checks. Make sure
check()calls are tied to a threshold on thechecksrate. Checks alone do not fail a test. - 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-executionnow makes Grafana Cloud secrets available throughsecrets.get()by default; pass--no-cloud-secretsto opt out. k6 cloud project listlists projects, with--format=jsonfor scripts.- Browser locator retries.
newAction-based Locator APIs in the browser module now retry automatically. - Better warnings. k6 now warns when
http.get()orhttp.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
- Pin your current version in CI so nothing upgrades by accident.
- Run the grep from step 1 and open one pull request per affected repository.
- Upgrade a single non-critical pipeline to 2.0 and run your smoke load tests.
- Compare results with the 1.x baseline. The summary format will look different, so compare metrics, not text.
- Update dashboards and alerts for FID, OTel rate counters and any parsing of summaries.
- 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.