skip to content

gRPC and WebSocket Calls

Driving gRPC and WebSocket traffic from a script: which import each needs, where a client is built versus connected, and why two different WebSocket modules still ship together.

on this pageshow

explore

questions

5

In k6 v2, which module provides the gRPC client, and what happens if a script imports k6/experimental/grpc?

level: juniorimportance: must knowfreq 58%

answer

  1. the experimental prefix is gone here
  2. net, not experimental
  3. the old name is a throwing stub
  4. fails in init, before any VU

basics

~20 s

k6 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 s

In 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 lines
javascript
import 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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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
open as a page

Which k6/net/grpc Client calls must run in the init context, and which are rejected there?

level: middleimportance: must knowfreq 62%

basics

~20 s

client.load() and client.loadProtoset() are init-only and error elsewhere with "load must be called in the init context". client.connect(), invoke(), asyncInvoke() and close() are the reverse: they raise an init-context error and must run inside an iteration.

open as a page

k6 v2 ships both k6/ws and k6/websockets as stable modules — how do the two APIs differ?

level: middleimportance: should knowfreq 46%

basics

~20 s

k6/ws exports a blocking ws.connect(url, params, callback) that parks the VU until the socket closes. k6/websockets exports a WebSocket constructor that returns at once and runs on k6's global event loop, so one VU can hold several sockets.

open as a page

Why does a non-OK RPC from k6's client.invoke() not fail the iteration, and how do you catch it?

level: seniorimportance: should knowfreq 51%

basics

~20 s

In k6, client.invoke() returns a Response for any gRPC status, so a non-OK code is data rather than an exception. Compare response.status against grpc.StatusOK in a check; nothing in k6 classifies a gRPC status as a failure for you.

open as a page

Your k6 suite mixes k6/ws and k6/websockets scripts — which would you standardise on, and why?

level: principalimportance: nice to knowfreq 33%

basics

~20 s

Both are stable in k6 v2, so nothing forces the choice. A defensible line is: ban the deprecated k6/experimental/websockets alias immediately, default new scripts to k6/websockets, and port existing k6/ws scripts only when they are being rewritten anyway.

open as a page