k6 v2 ships both k6/ws and k6/websockets as stable modules — how do the two APIs differ?
answer
- one blocks, one does not
- callback and Socket versus constructor
- global event loop, several sockets
- socket.on versus addEventListener
- same ws_ metric names either way
basics
~20 sk6/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.
solid answer
~30 sBoth are stable in k6 v2 and they are different APIs, not two names for one. `k6/ws` has a default export whose `connect(url, params, callback)` blocks the VU: you register handlers on the `Socket` it passes to the callback (`socket.on('open'|'message'|'close'|...)`), and the call only returns — with an HTTP-response-shaped object carrying `status` 101 — once the socket closes. `k6/websockets` exports `WebSocket` and `Blob`; `new WebSocket(url, protocols, params)` returns immediately with `readyState` `CONNECTING` and delivers events on k6's global event loop via `addEventListener` or `onmessage`, so a single VU can run several connections concurrently. Both emit the same `ws_*` built-in metrics.
code
javascript · 13 linesimport ws from 'k6/ws';
import { check } from 'k6';
export default function () {
const res = ws.connect('ws://127.0.0.1:9001/echo', null, function (socket) {
socket.on('open', () => socket.send('ping'));
socket.on('message', (msg) => {
console.log(`echoed: ${msg}`);
socket.close();
});
});
check(res, { 'upgraded': (r) => r && r.status === 101 });
}go deeper
Know that the import decides the API: import ws from 'k6/ws' gives you ws.connect(...) with a callback, and import { WebSocket } from 'k6/websockets' gives you new WebSocket(...) with event listeners.
Explain the blocking difference precisely: ws.connect() runs its own loop and returns only after the socket closes, while the WebSocket constructor returns straight away with readyState at CONNECTING.
Recognise the operational consequence when reading someone's script: on k6/ws a VU parked in connect() is doing nothing else, so per-VU behaviour differs sharply from the same script written on k6/websockets.
Two supported modules for one protocol is a standing cost. Weigh keeping both against a single house style, remembering that the metric names are identical so tooling does not have to change either way.
## Two stable modules, two different APIs k6 v2 registers **both** `k6/ws` and `k6/websockets` as stable built-ins. Neither is a replacement name for the other in the way `k6/net/grpc` replaced `k6/experimental/grpc` — they ship side by side and offer different programming models. The third name, `k6/experimental/websockets`, is a **deprecated alias** of `k6/websockets`: it still works and logs one warning per run. The export shapes differ, and that is the first thing you notice: - `k6/ws` exports a **default object** carrying a single function, `connect`. Scripts write `import ws from 'k6/ws'` and call `ws.connect(...)`. - `k6/websockets` exports the **named** `WebSocket` and `Blob`. Scripts write `import { WebSocket } from 'k6/websockets'` and call `new WebSocket(...)`. ## `k6/ws`: one blocking call that owns the VU `ws.connect(url, params, callback)` opens the connection and then does not return. The callback receives a `Socket`, you register handlers on it, and k6 runs a control loop that dispatches events until the socket closes — through `socket.close()`, a close frame from the server, or the VU being interrupted. Only then does `connect()` return, handing back an HTTP-response-shaped object with `url`, `status`, `headers`, `body` and `error`, so an upgraded connection can be asserted with `status === 101`. - Events available to `socket.on(...)`: `open`, `message`, `binaryMessage`, `ping`, `pong`, `close`, `error`. - Socket methods: `send`, `sendBinary`, `ping`, `close`, and its own `setTimeout` / `setInterval`, which only fire while the connection is open. - Because the VU is parked inside `connect()`, that VU cannot do anything else — including opening a second socket — for the whole session. ## `k6/websockets`: a constructor on k6's global event loop `new WebSocket(url, protocols, params)` returns **immediately**, with `readyState` at `CONNECTING`; the connection is established in the background and delivered as events on k6's global event loop. You attach behaviour with `addEventListener(type, handler)` or by assigning `onopen`, `onmessage`, `onerror`, `onclose`, `onping`, `onpong`. Because nothing blocks, a single VU can hold several sockets at once. Instance surface: `send`, `ping`, `close`, `addEventListener`, plus the properties `url`, `readyState`, `bufferedAmount`, `protocol`, `extensions` and `binaryType`. `binaryType` defaults to `"blob"` and only accepts `"blob"` or `"arraybuffer"`; anything else throws. Scheduling uses the ordinary global `setTimeout` / `setInterval`, not a socket method. ## Side by side | | `k6/ws` | `k6/websockets` | |---|---|---| | import | `import ws from 'k6/ws'` | `import { WebSocket } from 'k6/websockets'` | | entry point | `ws.connect(url, params, callback)` | `new WebSocket(url, protocols, params)` | | returns | an HTTP-response object, after close | the socket object, immediately | | handlers | `socket.on('message', fn)` | `addEventListener('message', fn)` / `onmessage` | | event loop | local, blocks the VU | k6's global loop | | sockets per VU | one at a time | several concurrently | | timers | `socket.setTimeout` / `setInterval` | global `setTimeout` / `setInterval` | | binary | `sendBinary`, `binaryMessage` event | `send` with `binaryType` `"blob"` or `"arraybuffer"` | ## What does not differ: the metrics Both modules push the **same six built-in WebSocket metrics**, so the numbers in your output do not change when you switch: - `ws_sessions` — counter of started sessions. - `ws_connecting` — trend, the duration of the connection request. - `ws_session_duration` — trend, how long the session lasted. - `ws_msgs_sent` and `ws_msgs_received` — counters of messages each way. - `ws_ping` — trend, ping to pong. That is a practical detail: any dashboard, filter or threshold expression keyed on those names survives a port between the two modules untouched. ## Writing the same echo session twice An echo session — connect, send one message, read it back, close — is expressible in both, and writing it both ways is the fastest way to feel the difference: 1. On `k6/ws`, call `ws.connect(url, null, cb)`. Inside `cb`, register `socket.on('open', ...)` to send the message and `socket.on('message', ...)` to read it and call `socket.close()`. Everything after the `connect()` line in your default function waits for that close. 2. On `k6/websockets`, call `new WebSocket(url)`, then `addEventListener('open', ...)` to send and `addEventListener('message', ...)` to read and `close()`. The default function reaches its last line immediately; the handlers fire afterwards, on the event loop. 3. In both cases the message text arrives differently: `k6/ws` passes the payload straight to the handler, while `k6/websockets` follows the browser shape and passes an event whose `data` property holds it. k6's own documentation recommends `k6/websockets` "when possible" for consistency with the rest of the API, while keeping `k6/ws` fully supported for the scripts that already use it. Neither carries a removal notice in v2, so a suite can legitimately contain both — it just should not contain both inside one script.
- Is k6/experimental/websockets a third implementation?No, it is a deprecated alias of `k6/websockets`. In k6 v2 it still resolves and behaves identically, but k6 logs one warning per run telling you to change the import. Unlike `k6/experimental/grpc`, which throws, this one does not stop the script.
- Which module can a script use to time out an idle socket?Both, but differently. `k6/ws` gives the `Socket` its own `setTimeout` and `setInterval`, which only fire while the connection is open. `k6/websockets` has no such methods — you use the ordinary global `setTimeout` and `setInterval`, since timers are globally available in k6 v2.
saying these in an interview costs you the question
- Says k6/ws was removed or deprecated in k6 v2
- Thinks k6/websockets is just a renamed k6/ws with the same call shape
- Expects new WebSocket() to block until the connection opens
- Assumes switching modules renames or drops the ws_ metrics
- Calls socket.on() on a k6/websockets instance
- Believes one VU can open several sockets through ws.connect()