Your k6 suite mixes k6/ws and k6/websockets scripts — which would you standardise on, and why?
answer
- both are supported, so nothing forces it
- the alias goes first
- porting is a rewrite, not a rename
- metric names survive either way
- the 101 assertion has no counterpart
basics
~20 sBoth 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.
solid answer
~40 sNeither module is going away — `k6/ws` and `k6/websockets` are both stable in k6 v2 — so this is a convention decision, not a deadline. I would kill `k6/experimental/websockets` first, since it is a deprecated alias that warns on every run and swapping the import is risk-free. For new work I would default to `k6/websockets`: it is what k6's docs recommend, it uses the global event loop, and a VU is not pinned inside a blocking `connect()`. I would not mass-port working `k6/ws` scripts, because the change is a rewrite — `socket.on` becomes `addEventListener`, `socket.setTimeout` becomes the global timer, and the `resp.status === 101` assertion has no counterpart. The `ws_*` metric names are identical either way, so reporting is not an argument for either side.
code
javascript · 10 linesimport { WebSocket } from 'k6/websockets';
export default function () {
// one VU, three echo sockets at once - impossible with a blocking ws.connect()
for (let i = 0; i < 3; i++) {
const socket = new WebSocket('ws://127.0.0.1:9001/echo');
socket.addEventListener('open', () => socket.send(`ping ${i}`));
socket.addEventListener('message', () => socket.close());
}
}go deeper
You are unlikely to own this call, but know the ground truth behind it: both module names work in k6 v2, and only k6/experimental/websockets prints a warning telling you to change it.
Be able to enumerate what actually changes in a port — the import, the entry point, the handler registration, the timers — and what does not, which is the metric names.
Argue the case from the concrete edges: the missing upgrade-status assertion, the socket-scoped timers, and the fact that a blocking connect keeps a VU inside one call for the whole session.
Write the default down and scope the migration by opportunity rather than as a project, because both modules are supported indefinitely and an unstated default leaves the suite carrying two idioms forever.
## The decision is real because nothing forces it k6 v2 registers `k6/ws` and `k6/websockets` as **stable** built-ins. Neither is on a removal path, so no deadline picks the winner for you; this is not the `k6/experimental/grpc` situation, where the old import throws and the choice is made. What you actually decide is whether the suite keeps two idioms or converges on one, and what you are willing to pay to converge. k6's own documentation makes a recommendation without forcing it: use `k6/websockets` **when possible**, because it runs on k6's global event loop for consistency with the rest of the API. That is a nudge, not a migration notice. ## What the port costs — it is a rewrite, not a rename Changing the import string alone does not compile. Every construct in a `k6/ws` script has a different counterpart: | `k6/ws` construct | `k6/websockets` counterpart | |---|---| | `import ws from 'k6/ws'` | `import { WebSocket } from 'k6/websockets'` | | `ws.connect(url, params, cb)` | `new WebSocket(url, protocols, params)` | | the `Socket` passed to the callback | the object the constructor returns | | `socket.on('message', fn)` | `addEventListener('message', fn)` or `onmessage` | | `socket.setTimeout` / `socket.setInterval` | global `setTimeout` / `setInterval` | | `socket.sendBinary(data)` | `send(data)` with `binaryType` set | | `resp.status === 101` after close | no response object; use an `open` listener | That last row is the one people forget. `ws.connect()` returns an HTTP-response-shaped object once the socket closes, and plenty of suites assert `status === 101` on it. The constructor returns the socket immediately and returns no response at all, so the assertion has to be rebuilt as an `open`-event check. ## What survives the port untouched The metric names do not change. Both modules push the same six built-ins: - `ws_sessions` — a counter of started sessions. - `ws_connecting` — a trend covering the connection request. - `ws_session_duration` — a trend covering the whole session. - `ws_msgs_sent` — a counter of messages the script sent. - `ws_msgs_received` — a counter of messages the server sent back. - `ws_ping` — a trend from ping to pong. So dashboards, output filters and threshold expressions keyed on those names keep working across a mixed suite and across a migration, and they keep working mid-migration too. Reporting continuity is therefore *not* an argument for either side, which removes the one objection that usually settles this kind of question by itself. ## What each side actually buys Laid out plainly, the arguments are short on both sides: - **For `k6/websockets`:** it is the documented recommendation; the constructor-and-listener shape is the one most engineers already know; a VU is not pinned to a single socket; and it uses the ordinary global timers rather than socket-scoped ones. - **For leaving `k6/ws` alone:** it is stable and produces no warning; the scripts on it already assert what they need to; and the port has genuine edges rather than being mechanical. - **Neutral either way:** the metric names, the parameter concepts (headers, tags, cookie jar, compression), and everything downstream that reads the output. ## The behavioural difference that is not cosmetic Under `k6/ws`, `connect()` runs its own loop and the VU stays inside that call until the socket closes: its default function does not iterate again, and it cannot open a second socket. Under `k6/websockets`, the constructor returns immediately and the same VU can hold several sockets in one iteration. So the two modules make the same script mean different things about a VU, and a suite that mixes them is mixing two definitions of what a VU is doing. That is the strongest argument for converging, and it is why a half-migration is worse than either endpoint. ## A defensible position 1. **Retire `k6/experimental/websockets` first, everywhere.** It is a deprecated alias of `k6/websockets` — same behaviour, plus a warning per run. Changing that import is a pure string edit with no risk, and it removes noise from every log. 2. **Make `k6/websockets` the default for new scripts.** It matches the recommendation, it is the API most engineers already know from browsers, and it does not pin a VU to one socket. 3. **Leave working `k6/ws` scripts alone unless something forces a rewrite.** They are on a supported module. Rewriting an asserting suite to gain nothing measurable is churn, and the port has real edges — the `101` assertion, the timer methods, the binary path. 4. **Do not straddle within one script or one scenario.** Whichever module a script uses, it uses for the whole file. ## What to write down The team-level artefact is short: which module new work uses, that `k6/experimental/websockets` is banned outright, that a script picks one module, and that a `k6/ws` script gets ported when it is being substantially changed anyway rather than as a standalone task. The point of writing it down is that both modules are stable and both will keep working, so without a stated default the suite drifts into holding two idioms permanently.
- Why retire k6/experimental/websockets before anything else?It is a deprecated alias of `k6/websockets` with identical behaviour, so the change is a one-string edit with nothing to re-test. It also stops k6 logging a deprecation warning on every run of those scripts, which is noise that trains people to ignore warnings.
- What breaks silently when porting a k6/ws script?The upgrade assertion. `ws.connect()` returns a response-shaped object after the socket closes, so scripts commonly check `status === 101`. The `WebSocket` constructor returns no such object, so that assertion has to be rebuilt as an `open`-event listener or it simply disappears from the suite.
- Does standardising break existing dashboards or thresholds?No. Both modules push the same six built-in metrics — `ws_sessions`, `ws_connecting`, `ws_session_duration`, `ws_msgs_sent`, `ws_msgs_received` and `ws_ping` — so anything keyed on those names keeps working through a migration and across a mixed suite.
saying these in an interview costs you the question
- Claims k6/ws is deprecated or removed and must be migrated urgently
- Treats the port as a find-and-replace on the import string
- Assumes the ws_ metric names change with the module
- Mixes both modules inside one script or scenario
- Keeps k6/experimental/websockets because it still works