In k6 v2, which module provides the gRPC client, and what happens if a script imports k6/experimental/grpc?
answer
- the experimental prefix is gone here
- net, not experimental
- the old name is a throwing stub
- fails in init, before any VU
basics
~20 sk6 v2 ships its gRPC client at the stable path k6/net/grpc. The old k6/experimental/grpc name is registered only to throw a migration error on import, so the run aborts in the init phase before any VU starts.
solid answer
~30 sIn k6 v2 the gRPC client is at `k6/net/grpc`, which exports `Client`, `Stream` and the `grpc.Status*` constants; you can write `import grpc from 'k6/net/grpc'` or `import { Client } from 'k6/net/grpc'`. The experimental path graduated, and k6 keeps `k6/experimental/grpc` registered purely as a stub that throws "k6/experimental/grpc has been graduated, please use k6/net/grpc instead". Because that happens while k6 builds the module graph, the failure is an init-phase abort, not a warning — no iteration runs. The API itself did not change, so migrating is a one-line path edit.
code
javascript · 12 linesimport grpc from 'k6/net/grpc';
import { check } from 'k6';
const client = new grpc.Client();
client.load([], 'echo.proto');
export default function () {
client.connect('127.0.0.1:9000', { plaintext: true });
const res = client.invoke('echo.Echo/Say', { text: 'ping' });
check(res, { 'status is OK': (r) => r && r.status === grpc.StatusOK });
client.close();
}go deeper
Memorise the working import: k6/net/grpc, giving you grpc.Client and grpc.StatusOK. If a tutorial shows k6/experimental/grpc, it predates k6 v2 and the script will not start.
Be able to say why the old name throws instead of warning: it is registered as a removed module, and module instantiation happens in init, so the whole run aborts before traffic is generated.
When you inherit an old suite, sweep for every k6/experimental/ import and classify each as stable, deprecated or removed — the three behave differently, and only removed ones stop the run.
Decide how the team keeps module paths current: a lint rule or a CI smoke run on every script costs seconds, because a removed import fails in init long before any load is generated.
## Where the gRPC client lives in k6 v2 k6 does not resolve its built-in modules from disk; it looks each name up in an internal table and hands back a Go-backed module. In **k6 v2** the entry that returns a gRPC client is **`k6/net/grpc`**. It exposes `Client`, `Stream`, and a family of status constants — `grpc.StatusOK`, `grpc.StatusNotFound`, `grpc.StatusUnavailable`, `grpc.StatusDeadlineExceeded` and the rest — plus the `HealthCheck*` serving statuses. Both import styles work, because when a built-in declares only named exports k6 assembles a default export object out of them: ```javascript import grpc from 'k6/net/grpc'; // grpc.Client, grpc.Stream, grpc.StatusOK import { Client, Stream } from 'k6/net/grpc'; // the same objects, unbundled ``` Everything a script does hangs off `Client`: - `client.load(importPaths, ...protoFiles)` — parse `.proto` definitions so k6 knows the message shapes. - `client.connect(address [, params])` — open the connection to `host:port`. - `client.invoke(method, request [, params])` — one unary call, returning a `Response`. - `client.asyncInvoke(...)` — the same call, returning a promise instead. - `client.close()` — hang up. ## `k6/experimental/grpc` throws — it is not a soft landing The old experimental name is **still registered in k6 v2, as a removed module**. Importing it does not warn and continue; it immediately throws a migration message that names the replacement: > `k6/experimental/grpc has been graduated, please use k6/net/grpc instead.` Module instantiation happens while k6 is building the script's module graph, so the failure lands in the **init phase**: the run aborts before a single VU iterates and no results are produced. There is no flag that restores the old path — the fix is to edit the import. ## The four states a k6 module name can be in | state | k6 v2 examples | what an import does | |---|---|---| | stable | `k6/net/grpc`, `k6/websockets`, `k6/ws`, `k6/http` | resolves silently | | experimental | `k6/experimental/csv`, `k6/experimental/fs`, `k6/experimental/streams` | resolves; API may still change | | deprecated | `k6/experimental/websockets` | resolves and works, logs a one-time warning | | removed | `k6/experimental/grpc`, `k6/experimental/browser`, `k6/experimental/redis` | throws a migration error | Exactly **three** experimental modules survive in v2, and gRPC is not one of them. ## Being present in the module table is not the same as existing The most common way to get this wrong is to search k6's module table, find the string `k6/experimental/grpc`, and conclude the path still resolves. It is there *precisely so the error can be raised*: a removed name is kept as a stub whose only job is to throw a message pointing at its replacement. The same holds for `k6/experimental/browser` (now `k6/browser`) and `k6/experimental/redis` (now an extension at `k6/x/redis`). Extensions are always `k6/x/`-prefixed and are never built in. ## The WebSocket names next door do not follow the same rule It is tempting to generalise "graduated means the experimental name throws", but k6 v2 does not apply one policy across the namespace: - **`k6/ws`** — stable. The original callback-and-`Socket` client. - **`k6/websockets`** — stable. The `WebSocket` constructor modelled on the browser API. - **`k6/experimental/websockets`** — **deprecated, not removed**. It still resolves and still works; it logs one warning per run telling you to switch to `k6/websockets`. So a script that imports `k6/experimental/grpc` cannot start, while a script that imports `k6/experimental/websockets` runs to completion with a warning in the log. Reading a single "experimental modules were graduated" line and applying it uniformly produces the wrong prediction in both directions. ## What the failure looks like in a run The abort is loud and specific, which is the one mercy here. k6 prints the migration text as the error, names the import that caused it, and exits before any summary is produced — there is no result file, no metrics, and no VU output to misread as "the test ran but found nothing". A run that produced no summary at all should send you to the imports first, because a throw during init is one of the few things that stops k6 that early. Contrast that with the deprecated case: `k6/experimental/websockets` logs its warning once, on the first VU that instantiates it, and the run then proceeds normally and produces a full summary. A warning in a long log is easy to miss; an init abort is not. ## Migrating an older script 1. Grep the suite for `k6/experimental/` and list every hit. 2. For gRPC, change **only the path** — the API graduated unchanged, so `new grpc.Client()`, `client.load()`, `client.connect()`, `client.invoke()` and `grpc.StatusOK` all keep working. 3. Run the script once. A removed import fails in init before any traffic is generated, so this costs seconds and tells you immediately whether any stale path is left. The payoff for getting the path right is that everything downstream — the `Response` object, the `grpc.Status*` constants, and the `grpc_req_duration` measurement k6 records for each call — becomes available. Get it wrong and none of it runs.
- Does the same removal rule apply to k6/experimental/websockets?No. `k6/experimental/websockets` is deprecated, not removed: it still resolves and still works, and k6 logs a single warning per run pointing at `k6/websockets`. Only `k6/experimental/grpc`, `k6/experimental/browser`, `k6/experimental/redis`, `k6/experimental/timers`, `k6/experimental/tracing` and `k6/experimental/webcrypto` throw on import in v2.
- Which modules are still genuinely experimental in k6 v2?Exactly three: `k6/experimental/csv`, `k6/experimental/fs` and `k6/experimental/streams`. Everything else under that prefix either graduated to a stable path or was moved out to an extension, which always carries a `k6/x/` prefix rather than a built-in name.
- What does k6/net/grpc actually export?`Client` and `Stream` as constructors, the gRPC status constants (`StatusOK`, `StatusNotFound`, `StatusUnavailable`, `StatusDeadlineExceeded` and the rest of the set), and the `HealthCheck*` serving-status values. k6 synthesises a default export object from those names, so a default import and a named import reach the same objects.
It is a forwarding address that no longer forwards. The old name is still on the register, but all it does is hand you a card with the new address on it.
saying these in an interview costs you the question
- Claims k6/experimental/grpc still works and only prints a deprecation warning
- Says the path is k6/grpc or k6/experimental/net/grpc
- Assumes finding the old name in k6's module table proves it resolves
- Thinks migrating from the experimental path needs API changes, not just a path edit
- Expects the removed-import failure at first request rather than in init