skip to content

In k6 v2, why does k6/experimental/websockets only warn while k6/experimental/grpc throws?

level: middleimportance: should knowfreq 48%

answer

  1. two entries, one registry, different bindings
  2. one wraps the real module, one only throws
  3. warned once per process, not per VU
  4. deprecation is guaranteed until the next major

basics

~20 s

k6/experimental/websockets is deprecated, not removed: k6 v2 registers it as a wrapper that logs one notice and then hands back the same websockets implementation that k6/websockets exposes. k6/experimental/grpc is registered only to a throwing stub.

solid answer

~40 s

They are two different kinds of entry in the same registry. `k6/experimental/websockets` is bound to a thin wrapper around the same websockets implementation that `k6/websockets` exposes: it logs its deprecation notice once and then delegates, so the API, the metrics and the behaviour are the same. `k6/experimental/grpc` is bound to a stub that has no module behind it and only throws a migration message naming `k6/net/grpc`. The difference is policy, not accident: k6 guarantees a deprecated API for the whole lifetime of the major version it was deprecated in, so the websockets alias will keep working across every v2.x release and can only disappear at v3.0.0, while the removed paths already went at the v2.0.0 boundary.

code

javascript · 8 lines
javascript
// Deprecated: still works, logs one notice per k6 process
import { WebSocket as Old } from 'k6/experimental/websockets';
// Current path, same implementation, no notice
import { WebSocket } from 'k6/websockets';

export default function () {
  console.log(typeof Old, typeof WebSocket);
}

go deeper

for a junior

Know that a warning line and a failed run mean different things in k6. The deprecated websockets path still works; the removed grpc path stops the run before it starts.

for a middle

Explain the two registry entries: one wraps the real module and logs once, the other only throws. Tie that to k6's rule that a deprecated API survives the whole major version.

for a senior

Point out that one log line in a long run is easy to miss and that the exit code is unaffected, so a source scan is the reliable way to know what your suite still imports.

for a principal

Decide how your team treats deprecation notices: whether a k6 warning is allowed to sit in the logs until the next major, or is tracked as scheduled work with a named owner.

## Two different registry entries k6's built-in module registry maps an import path to a Go module object. In k6 v2 the old `k6/experimental/` names are still keys in that map, but they are bound to two different things: - **Deprecated.** `k6/experimental/websockets` is bound to a wrapper that holds the *real* `k6/websockets` module. On first use it logs `k6/experimental/websockets is deprecated and will be removed in a future release. Please use k6/websockets instead.` and then delegates. There is no second implementation behind the alias; both paths hand your script the same API. - **Removed.** `k6/experimental/grpc` is bound to a stub with no module behind it. Instantiating it raises an error saying the module has graduated and to use `k6/net/grpc`. The run stops there. So the observable difference -- a warning you can ignore versus a run that dies during load -- is just which of those two entries the k6 team wrote for that path. ## What deprecation buys you k6 publishes a versioning policy, and the deprecation clause is the part that matters here: deprecated APIs remain available during their deprecation phase and are **maintained for the entire lifecycle of the current major version**. Removal is a breaking change, and breaking changes wait for a major. The practical reading: | status | example | what you see on import | earliest it can stop working | |---|---|---|---| | core | `k6/websockets` | nothing | k6 v3.0.0 | | experimental | `k6/experimental/fs` | nothing | any v2.x minor | | deprecated | `k6/experimental/websockets` | one log line | k6 v3.0.0 | | removed | `k6/experimental/grpc` | a throw naming the replacement | already gone | | unregistered | an extension path on a stock binary | an unknown-modules error | not applicable | Note the asymmetry that catches people out: an *experimental* module carries a weaker promise than a *deprecated* one. The deprecated alias is safe for the rest of the v2 line; the experimental module can change in the next six-weekly minor. ## Why you only see the warning once The wrapper guards its log call so the notice is emitted **once per k6 process**, not once per VU and not once per iteration. That is deliberate -- a thousand VUs importing a deprecated module would otherwise flood the log -- but it has a consequence worth planning for: 1. One line, early, in what may be a very long run's output. 2. It is trivially lost in CI logs, especially with a progress bar scrolling above it. 3. It never changes the exit code, so a green pipeline tells you nothing about whether the deprecation is in your scripts. If you want to know whether your suite still uses the alias, scan the sources for the path rather than watching the run output. A source scan is deterministic; a single log line is not. ## Superseded is a third status again Do not fold "superseded" into either bucket. `k6/ws` and `k6/crypto` are **core** modules that the documentation recommends against in favour of `k6/websockets` and the global `crypto` object. They import with no warning, they are on the stability allowlist, and nothing about them will break in the v2 line. A recommendation to prefer something newer is not a deprecation. That gives four things a path under discussion might be, and they need different reactions: - core and current -- leave it alone; - core but superseded -- migrate when convenient, nothing is forcing you; - deprecated -- migrate before the next major, the warning is your notice; - removed -- migrate now, the run does not start. ## How to tell which one you are looking at Run the script. A path that prints nothing is core or experimental; a path that prints one notice is deprecated; a path that kills the load with a message naming a replacement is removed; a path that produces an unknown-modules error was never registered at all. The error text distinguishes the last two reliably, which is the point of k6 keeping the removed names in the registry rather than letting them fall through to a generic resolution failure. ## One implementation, two registrations A precise detail that occasionally trips people up: the alias and the core path are two separate entries in the registry, each carrying its own module built from the same implementation. They behave identically, emit the same metrics and expose the same API, but they are not one shared object -- importing both paths in the same script gives you two bindings, not one. That is harmless and pointless in equal measure. There is no reason to keep the alias once you know about it, and no reason to panic about it either: the correct reaction to the notice is a scheduled specifier change, not an incident.

  • Does k6/experimental/websockets behave differently from k6/websockets?
    No. In k6 v2 the deprecated path is registered as a wrapper holding a websockets module of the same implementation `k6/websockets` exposes. It logs its notice, then delegates every call. API, metrics and behaviour are identical; only the log line differs.
  • When can k6 actually delete the deprecated websockets alias?
    Not before k6 v3.0.0. k6's versioning policy guarantees a deprecated API for the whole lifetime of the major version it was deprecated in, and a removal is a breaking change that has to wait for a major. Majors ship roughly once a year.
  • Why is a deprecated k6 path a safer dependency than an experimental one?
    Because the guarantees run opposite ways. A deprecated path is promised for the rest of the current major line, while an experimental module is excluded from the stability allowlist and may change in any six-weekly minor. The warning is louder; the risk is lower.

A removed import path is a disconnected phone number whose recording gives you the new one. A deprecated path is the old number that still rings through to the same desk, after a short notice saying it will not do so forever.

saying these in an interview costs you the question

  • Treats a k6 deprecation warning as a failed run
  • Thinks the deprecation notice is logged once per VU
  • Assumes every k6/experimental path throws in v2
  • Believes a deprecated k6 path may vanish in a patch release
  • Thinks the deprecated alias exposes an older or smaller API