Which k6/net/grpc Client calls must run in the init context, and which are rejected there?
answer
- the client straddles two phases
- schema early, socket late
- load is the init-only half
- connect and invoke need VU state
- __ITER === 0 guards the dial
basics
~20 sclient.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.
solid answer
~40 sThe `k6/net/grpc` client splits across k6's two phases. Parsing the schema — `client.load(importPaths, ...protoFiles)` or `client.loadProtoset(path)` — is **init-only**, so it sits at module scope next to `new grpc.Client()`; calling it from the default function fails with `load must be called in the init context`. Opening and using the connection is the opposite: `connect()`, `invoke()`, `asyncInvoke()` and `close()` need VU state and throw an init-context error if you try them at module scope. Calling `invoke()` before any `connect()` gives `no gRPC connection, you must call connect first`. Many scripts guard the dial with `if (__ITER === 0)` so each VU connects once.
code
javascript · 15 linesimport grpc from 'k6/net/grpc';
import { check } from 'k6';
// init context: load() is legal here and nowhere else
const client = new grpc.Client();
client.load([], 'echo.proto');
export default function () {
// iteration: connect() and invoke() are legal here and nowhere else
if (__ITER === 0) {
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 });
}go deeper
Copy the shape and remember which half is which: create the client and call load() at the top of the file, then connect() and invoke() inside the exported default function.
Explain the reason behind the split — descriptors are parsed once per VU, while a connection needs VU state that does not exist during init — and name the errors each violation produces.
Know the cost tradeoff you are choosing when you guard connect() with __ITER === 0 versus dialling every iteration, and be ready to spot a missing plaintext: true in someone else's failing script.
Set the house pattern for connection reuse in gRPC scripts, and say plainly what the run then measures, since a connection opened once per VU is no longer part of the per-call figure.
## Two phases, two halves of the client A k6 script has an **init context** — everything at module scope, evaluated once per VU before any iteration — and the **default function**, which runs once per iteration. The `k6/net/grpc` `Client` splits its methods cleanly across that line, and calling one on the wrong side is an error, not a warning. - **Init only:** `client.load(importPaths, ...protoFiles)` and `client.loadProtoset(path)`. - **Iteration only:** `client.connect(address [, params])`, `client.invoke(...)`, `client.asyncInvoke(...)`, `client.healthCheck(...)`. - **Either side of a call, but only during an iteration:** `client.close()`. Constructing the client itself — `new grpc.Client()` — is fine at module scope, and that is the normal place for it. ## Why `load()` is init-only `load()` compiles `.proto` sources into method descriptors and stores them on the client. k6 rejects the call outside init with `load must be called in the init context`. The reason is economic: descriptors are parsed once per VU and then reused for every iteration that VU runs. Moving the call into the default function would re-parse the schema on every iteration, and k6 removes the option rather than letting a script pay that cost by accident. The first argument is the list of import paths used to resolve `import` statements inside the proto files; passing `[]` or `null` means the current directory. The remaining arguments are the proto files themselves, as rest parameters. ## Why `connect()` and `invoke()` are the exact opposite A connection is per-VU network state and needs the VU's runtime state, which does not exist during init. k6 raises an init-context error with a plain message — *connecting to a gRPC server in the init context is not supported* — and the same for invoking. This gives the client an unusual shape compared with, say, a shared data array: the schema is set up once at module scope, the socket is opened inside the iteration. Two further guardrails apply at call time: 1. `invoke()` on a client that has never connected fails with `no gRPC connection, you must call connect first` — including on a client whose `close()` has already run, because `close()` drops the connection reference. 2. `invoke()` with a method that was never loaded fails with `method "…" not found in file descriptors`. The method name is written `package.Service/Method`; a leading `/` is optional, k6 adds it if missing. ## Connecting once instead of every iteration `connect()` is not free, so the common pattern guards it so that each VU dials once and reuses the connection for all of its iterations: ```javascript export default function () { if (__ITER === 0) { client.connect('127.0.0.1:9000', { plaintext: true }); } const res = client.invoke('echo.Echo/Say', { text: 'ping' }); } ``` The alternative — `connect()` at the top of every iteration and `close()` at the bottom — is also valid and is what most of the documentation examples show; it measures connection setup on every pass instead of amortising it. ## One default that catches everyone `connect()` is **TLS by default**: `plaintext` is `false`. Pointing a script at a local plaintext gRPC server without `{ plaintext: true }` fails the handshake, and the error looks like a connectivity problem rather than a configuration one. Other connect parameters are `timeout` (default 60s), `reflect` and `reflectMetadata` for server reflection, `maxReceiveSize`, `maxSendSize`, `authority`, and a `tls` object taking `cert`, `key`, `password` and `cacerts`. Both parameter objects are **strict**: an unrecognised key in the connect parameters is rejected with `unknown connect param`, and an unrecognised key in the per-call parameters (`metadata`, `tags`, `timeout`, `discardResponseMessage`) with `unknown param`. A typo does not get silently dropped. ## The errors, so you recognise them on sight - `load must be called in the init context` — `load()` or `loadProtoset()` ran inside an iteration. - `connecting to a gRPC server in the init context is not supported` — `connect()` ran at module scope. - `invoking RPC methods in the init context is not supported` — `invoke()` ran at module scope. - `no gRPC connection, you must call connect first` — the iteration reached `invoke()` with no dial. - `method "…" not found in file descriptors` — the method name is wrong, or its proto was never loaded. ## Streams follow the same rule `new Stream(client, 'package.Service/Method' [, params])` is constructed inside the iteration and validates the client the same way — an unconnected client produces `no gRPC connection, you must call connect first`. The stream object exposes `on('data' | 'error' | 'end', handler)`, `write(message)` and `end()`, and its method must have been loaded in init just like a unary one. ## The checklist 1. `new grpc.Client()` and `client.load(...)` at module scope. 2. `client.connect(...)` inside the default function, with `plaintext: true` for a plaintext target. 3. `client.invoke(...)` — or `new Stream(client, ...)` — after the connection exists. 4. `client.close()` when the VU is done with the connection.
- What does the first argument of client.load() do?It is the list of import paths used to resolve `import` statements inside the proto files. Passing `[]` or `null` means the current working directory is the only import path. The remaining arguments are rest parameters naming the proto files themselves, so `client.load(['../googleapis'], 'a.proto', 'b.proto')` is valid.
- Why does a k6 script against a local gRPC server fail even though the port is open?`connect()` defaults to TLS — `plaintext` is `false`. A plaintext server needs `client.connect('127.0.0.1:9000', { plaintext: true })`. Without it the handshake fails and the error reads like a connectivity problem, which is why this is the most common first-run failure.
- What happens if you misspell a key in the connect parameters?It is rejected, not ignored. k6 fails the call with `unknown connect param: "…"`. The per-call parameters passed to `invoke()` behave the same way, accepting only `metadata`, `tags`, `timeout` and `discardResponseMessage`.
The phrasebook is printed before the shift starts and reused all day; the phone call can only be made once the shift is running. Swapping the two gets you an error either way.
saying these in an interview costs you the question
- Calls client.load() inside the default function to pick up schema changes
- Calls client.connect() at module scope beside new grpc.Client()
- Assumes invoke() opens the connection lazily if none exists
- Forgets plaintext: true against a non-TLS gRPC target
- Expects an unknown connect parameter to be silently ignored