Resource library

QA How-To

k6 vs Artillery WebSocket Load Testing (2026)

Compare k6 vs artillery websocket load testing with runnable scripts, workload models, metrics, CI gates, production trade-offs, and a practical 2026 verdict.

22 min read | 3,165 words

TL;DR

k6 is the stronger default for engineering-heavy WebSocket performance tests because its JavaScript API, custom metrics, thresholds, and concurrency executors provide precise control. Artillery is often faster to adopt for Node.js teams that prefer YAML journeys and arrival-rate workloads. Both can open long-lived connections and send messages, so the decisive factor is the load model and the assertions your release process needs.

Key Takeaways

  • Choose k6 when percentile thresholds, custom message latency, and concurrency-shaped scenarios are central to your test.
  • Choose Artillery when a readable YAML scenario and arrival-rate modeling help a JavaScript team move faster.
  • Use k6/websockets instead of the deprecated k6/experimental/websockets import in new 2026 scripts.
  • Compare the same session duration, message cadence, payload, connection ramp, and test location before comparing tools.
  • Measure connection time, delivery errors, message round-trip time, session duration, and server saturation together.
  • Treat an echo test as harness validation, then replace it with your application's authentication and message protocol.
  • Add machine-enforced thresholds so a visually plausible report cannot silently pass a regression.

k6 vs artillery websocket load testing is not a contest about which CLI can open a socket. Both tools can create long-lived WebSocket sessions, send text or JSON messages, and report connection activity. The practical difference is how you express concurrency, correlate replies, define service-level objectives, and explain the test to the team that will maintain it.

Use k6 when you want code-level control over each connection, first-class custom trends and rates, and thresholds that fit a performance engineering workflow. Use Artillery when your team values concise YAML scenarios, thinks in virtual-user arrivals, and already works in the Node.js ecosystem. This guide runs both against the same local echo service, then turns the examples into realistic load tests.

An echo server proves the generator, network path, and metric wiring. It does not prove that your production chat, trading, notification, or collaboration service is healthy. After the examples work, substitute the real authentication handshake, message schema, subscription flow, and business success criteria. If you need the broader method behind that transition, read the k6 performance engineering guide.

k6 vs artillery websocket load testing: TL;DR verdict

Decision area k6 Artillery Practical winner
Test authoring JavaScript with k6/websockets YAML, JavaScript, or TypeScript with the ws engine Artillery for simple flows, k6 for custom behavior
Load model Executors model fixed, ramping, or arrival-rate work Phases naturally launch arrivals per second Tie, if you translate the models correctly
Message correlation Direct event handlers make correlation explicit Basic YAML records send and receive counts; advanced correlation needs custom code k6
Built-in WebSocket metrics Connection time, sessions, session duration, sent and received messages, ping Sent and received counters plus send and receive rates k6 for connection analysis
Pass or fail rules Thresholds on built-in and custom metrics ensure thresholds and conditions Tie for basic gates, k6 for rich custom latency gates
Readability for mixed teams Requires JavaScript literacy Short declarative YAML is easy to review Artillery
Concurrent sockets per VU Global event loop can manage multiple sockets Common model is one socket per virtual user scenario k6
Node package integration k6 runtime, not Node.js Processor code can use the Node.js ecosystem Artillery

The default recommendation is k6 for a test suite owned by performance engineers or SDETs. Pick Artillery when product engineers will own readable scenario files and the workload is naturally described as new user arrivals. Do not choose from an online request-per-second benchmark. WebSocket tests are dominated by open-session count, session lifetime, message cadence, payload size, fan-out, and server behavior.

What You Will Build

By the end, you will have:

  • A disposable WebSocket echo service on ws://localhost:10000.
  • A one-user k6 smoke test using the stable k6/websockets module.
  • A one-user Artillery smoke test using its built-in ws engine.
  • A k6 workload that holds concurrent connections and measures message round-trip time.
  • An Artillery workload that launches sessions at a controlled arrival rate and enforces delivery conditions.
  • A comparison worksheet that prevents false conclusions from mismatched workloads.

All examples use text frames containing JSON. Binary frames, compression, Socket.IO, and application-level acknowledgements change the workload. Socket.IO is not raw WebSocket even though it can use WebSocket as a transport, so use Artillery's Socket.IO engine or a purpose-built client when the server speaks that protocol.

Prerequisites

Use Docker 27 or newer, k6 2.1.x, Node.js 24 LTS, and Artillery 2.0.33. A newer compatible patch release is fine, but record the exact generator versions with every saved result. You need roughly one CPU core and a few hundred megabytes for this small local exercise. Large connection tests need operating-system socket tuning and separate generator hosts.

Install k6 with the package documented for your operating system. Run Artillery through npx so the tutorial does not require a global package. Check all tools before creating tests:

docker version --format '{{.Server.Version}}'
k6 version
node --version
npx artillery@2.0.33 version

Verify that Docker returns a server version, k6 reports v2.1.x, Node reports v24.x, and Artillery reports 2.0.33. Stop here if Docker cannot reach its daemon. On a corporate laptop, also confirm that endpoint security does not block localhost WebSocket upgrades.

Step 1: Start a deterministic WebSocket target

Run a local echo server. It returns every text frame to the same connection, which gives both clients a simple, observable success condition.

docker run --detach --rm --name ws-echo-server -p 10000:8080 jmalloc/echo-server

The container maps host port 10000 to port 8080 in the server. Keeping the target local removes internet latency and third-party rate limits from the tool comparison. It also means the generator and service compete for the same machine, so these numbers are for correctness, not capacity planning.

Verify the container and port:

docker ps --filter name=ws-echo-server --format '{{.Names}} {{.Status}} {{.Ports}}'

Expect one line containing ws-echo-server, an Up status, and 0.0.0.0:10000->8080/tcp or the equivalent IPv6 mapping. If the port is already allocated, remove or stop the conflicting process, or consistently change 10000 in every later file.

Step 2: Create and verify the k6 WebSocket smoke test

Save this as k6-smoke.js. The 2026 import is k6/websockets. Do not copy older examples that use k6/experimental/websockets; that path is deprecated. The global event loop used by the current module also allows one VU to manage multiple concurrent connections when a later scenario needs it.

import { check } from 'k6';
import { Rate } from 'k6/metrics';
import { WebSocket } from 'k6/websockets';

const echoedMessages = new Rate('echoed_messages');

export const options = {
  vus: 1,
  iterations: 1,
  thresholds: {
    echoed_messages: ['rate==1'],
    ws_connecting: ['p(95)<500'],
  },
};

export default function () {
  const expected = JSON.stringify({ type: 'smoke', id: `vu-${__VU}` });
  const socket = new WebSocket('ws://localhost:10000', null, {
    tags: { test_type: 'smoke' },
  });

  const timeoutId = setTimeout(() => {
    echoedMessages.add(false);
    socket.close();
  }, 5000);

  socket.onopen = () => socket.send(expected);

  socket.onmessage = (event) => {
    const matched = check(event.data, {
      'echo payload matches': (data) => data === expected,
    });
    echoedMessages.add(matched);
    clearTimeout(timeoutId);
    socket.close();
  };

  socket.onerror = () => {
    echoedMessages.add(false);
  };
}

The custom Rate turns payload correctness into a release gate. The timeout closes a silent connection after five seconds, preventing a broken target from leaving the VU waiting until the outer scenario limit. ws_connecting is a built-in trend measured in milliseconds.

Verify the step:

k6 run k6-smoke.js

Expect one passed check, echoed_messages at 100.00%, one sent message, one received message, and a zero exit status. A successful HTTP upgrade without the echoed payload is a failed test, not a partial pass. For a deeper treatment of checks and gates, see k6 thresholds and checks.

Step 3: Create and verify the Artillery WebSocket smoke test

Save the following as artillery-smoke.yml. Artillery's ws engine opens a connection automatically when the flow begins. The explicit connect action makes the target visible and leaves a clean place to add query parameters later.

config:
  target: 'ws://localhost:10000'
  phases:
    - duration: 1
      arrivalCount: 1
      name: 'one connection'
  plugins:
    ensure:
      thresholds:
        - 'vusers.failed': 1
      conditions:
        - expression: 'websocket.messages_received >= 1'
scenarios:
  - name: 'echo smoke'
    engine: ws
    flow:
      - connect: '{{ target }}'
      - send:
          type: 'smoke'
          client: '{{ $uuid }}'
      - think: 1

An object passed to send is serialized with JSON.stringify. The one-second think time leaves the connection open long enough for the echo to return. The ensure threshold requires fewer than one failed VU, while the condition requires at least one received message. These checks operate on the aggregate run.

Verify the step and save a machine-readable report:

npx artillery@2.0.33 run --output artillery-smoke-report.json artillery-smoke.yml

Expect websocket.messages_sent: 1, websocket.messages_received: 1, vusers.completed: 1, and vusers.failed: 0. The command must exit with status zero. If messages sent is one but received is zero, increase think only after checking server logs and protocol compatibility. A delay can hide a broken schema, so it is not the first fix.

Step 4: Model realistic concurrency with k6

Now save k6-load.js. This script ramps to 20 VUs, holds that concurrency, sends one message per second from each open session, measures application-level echo round-trip time, and closes each session after 20 seconds. Every new iteration opens a new connection, so the executor maintains the target VU count during the steady phase.

import { Rate, Trend } from 'k6/metrics';
import { WebSocket } from 'k6/websockets';

const echoRoundTrip = new Trend('echo_round_trip', true);
const messageErrors = new Rate('message_errors');

export const options = {
  scenarios: {
    websocket_sessions: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '10s', target: 20 },
        { duration: '30s', target: 20 },
        { duration: '10s', target: 0 },
      ],
      gracefulRampDown: '5s',
    },
  },
  thresholds: {
    ws_connecting: ['p(95)<500'],
    echo_round_trip: ['p(95)<250'],
    message_errors: ['rate<0.01'],
  },
};

export default function () {
  const socket = new WebSocket('ws://localhost:10000', null, {
    tags: { scenario: 'echo_load' },
  });
  let sequence = 0;

  socket.onopen = () => {
    const intervalId = setInterval(() => {
      sequence += 1;
      socket.send(JSON.stringify({
        client: `vu-${__VU}`,
        sequence,
        sentAt: Date.now(),
      }));
    }, 1000);

    setTimeout(() => {
      clearInterval(intervalId);
      socket.close(1000);
    }, 20000);
  };

  socket.onmessage = (event) => {
    try {
      const message = JSON.parse(event.data);
      const valid = message.client === `vu-${__VU}` &&
        Number.isInteger(message.sequence) &&
        Number.isFinite(message.sentAt);
      messageErrors.add(!valid);
      if (valid) echoRoundTrip.add(Date.now() - message.sentAt);
    } catch (error) {
      messageErrors.add(true);
    }
  };

  socket.onerror = () => messageErrors.add(true);
}

The boolean second argument to Trend marks values as time measurements. Correlation checks the VU identifier and sequence data before recording latency. A production protocol should correlate on a server acknowledgement ID, because broadcasts and out-of-order responses can make client timestamps alone ambiguous.

Verify the workload:

k6 run --summary-export k6-load-summary.json k6-load.js

Expect all three thresholds to pass. Confirm that ws_sessions is greater than 20 because replacement iterations start during the hold, and inspect ws_msgs_sent, ws_msgs_received, echo_round_trip, and message_errors. Do not expect exact message totals: timers near socket closure and ramp-down boundaries can change the count slightly. Learn how executor choice changes this behavior in k6 scenarios and executors.

Step 5: Model session arrivals with Artillery

Save artillery-load.yml. This example creates one new session per second for 40 seconds. Each session sends 20 messages one second apart, then stays connected for one final second so the last echo can arrive. Once the test reaches steady state, it has roughly 20 active sessions because arrival rate multiplied by session duration approximates concurrency.

config:
  target: 'ws://localhost:10000'
  phases:
    - duration: 10
      arrivalRate: 1
      name: 'connection warmup'
    - duration: 30
      arrivalRate: 1
      name: 'steady arrivals'
  plugins:
    ensure:
      thresholds:
        - 'vusers.failed': 1
      conditions:
        - expression: 'websocket.messages_received >= websocket.messages_sent'
  ws:
    headers:
      X-Test-Run: 'artillery-local-comparison'
scenarios:
  - name: 'twenty-message echo session'
    engine: ws
    flow:
      - connect: '{{ target }}'
      - loop:
          - send:
              type: 'load'
              client: '{{ $uuid }}'
          - think: 1
        count: 20
      - think: 1

Artillery phases describe arrivals, not a fixed pool of active users. The test launches 40 total sessions, while k6 continuously tries to hold a VU target during its steady stage. The two examples create a similar steady connection level, but they are intentionally not presented as a scientific head-to-head benchmark. To make them equivalent, export timestamps and active-connection telemetry from the service, then tune phase duration and session length until both active-connection curves overlap.

Verify the Artillery workload:

npx artillery@2.0.33 run --output artillery-load-report.json artillery-load.yml

Expect 40 created VUs, no failed VUs, about 800 messages sent, and at least as many received messages as sent. Echo implementations may send an initial informational frame, which is why the condition uses >=. Your real service should use stricter message-type counts rather than treating every inbound frame as a business acknowledgement.

Step 6: Compare k6 vs artillery websocket load testing results correctly

First compare workload facts, not headline rates. Record generator CPU, generator memory, test duration, created sessions, peak open sockets, message size, send interval, and server build. If any of those differ, a faster p95 may describe a lighter test rather than a better generator.

Use this review order:

  1. Confirm the server observed the intended active connections. Client-side session counts cannot reveal a load balancer that accepted and immediately displaced older sockets.
  2. Check connection failures and upgrade latency. A message metric says nothing about clients that never established a session.
  3. Compare sent, received, acknowledged, duplicated, and malformed messages by business type. Raw receive counts include heartbeats, subscription confirmations, and broadcasts.
  4. Inspect p50, p95, p99, and maximum message round-trip time. Averages conceal stalls caused by garbage collection, event-loop pauses, or overloaded broker partitions.
  5. Correlate client degradation with server CPU, memory, open file descriptors, event-loop lag, queue depth, and downstream latency.

k6 exposes built-in ws_connecting, ws_session_duration, ws_sessions, ws_msgs_sent, ws_msgs_received, and ws_ping metrics. Its custom Trend gives the example a true message round-trip distribution. Artillery's WebSocket engine reports websocket.messages_sent, websocket.messages_received, websocket.send_rate, and websocket.receive_rate, plus VU lifecycle metrics. Basic YAML does not automatically turn an echoed application message into a correlated response-time histogram. That distinction matters more than minor syntax preferences.

For a systematic investigation after a threshold fails, use the performance bottleneck workflow.

Step 7: Add authentication and production protocol behavior

Most real endpoints reject anonymous upgrades. In k6, pass upgrade headers through the constructor's third argument. In Artillery, add headers under config.ws.headers, or make connect an object when individual scenarios require distinct connection options. Supply secrets through environment variables or your CI secret store, never in the committed test.

A realistic session usually has four protocol states: authenticate, subscribe, exchange business messages, then leave gracefully. Do not start the load phase until the smoke test confirms every state transition. Validate close codes too. A server-initiated 1008 policy violation and a normal 1000 closure both reduce the active socket count, but only one is expected behavior.

Message round-trip time must start immediately before send and stop only on the matching acknowledgement. For a broadcast service, measure publisher-to-server acknowledgement separately from publisher-to-subscriber delivery. Stamp a unique test ID, VU ID, and sequence number into each message so logs can distinguish loss from delayed delivery. Avoid using wall clocks across separate generator hosts unless they are synchronized; same-process duration measurements are safer.

Finally, shape payloads from production distributions. A workload with 50-byte heartbeats does not predict the cost of 32 KB collaboration deltas, compression, authorization checks, or fan-out to thousands of subscribers. The API performance testing tutorial provides a useful model for turning traffic evidence into workload parameters.

Verify your adapted smoke test with one user before adding load:

K6_TOKEN='replace-at-runtime' k6 run k6-smoke.js
ARTILLERY_TOKEN='replace-at-runtime' npx artillery@2.0.33 run artillery-smoke.yml

The shown files do not yet read those variables, so this command is a verification pattern for your adapted versions, not permission to paste tokens into the sample. Confirm the server logs show the expected test identity and that no token appears in console output or saved reports.

Which Should You Choose

Choose k6 if the primary deliverable is an engineering-grade performance gate. Its standard-style WebSocket event API makes custom parsing and correlation direct. Executors let you choose fixed concurrency, ramping concurrency, or arrival rate without changing tools. Custom Trend, Rate, and Counter metrics can carry tags and thresholds, which makes a latency SLO such as message acknowledgement p95 under 250 ms explicit. k6 also fits teams already using Grafana-compatible observability and teams that want one scripting approach across HTTP, WebSocket, gRPC, and browser-level checks.

Choose Artillery if test readability and Node.js team ownership are more important than detailed protocol instrumentation. A product engineer can review a YAML flow of connect, send, think, and loop without learning a specialized runtime. Arrival phases map naturally to sign-in bursts, live-event joins, and notification-client reconnect storms. Processor functions are useful when test data or authentication already relies on Node packages. Artillery also has distinct engines for WebSocket and Socket.IO, which reduces the temptation to test a Socket.IO application with the wrong wire protocol.

Use both only when their roles are distinct. For example, Artillery can express product journeys owned by feature teams while k6 runs a specialized capacity and soak suite. Duplicating every test in both formats doubles maintenance and creates arguments about mismatched workloads. If your existing comparison includes JMeter, the JMeter vs k6 load testing guide helps frame the migration trade-offs.

Interview Questions and Answers

A strong interview answer starts with the load model, not a favorite tool. Explain whether the system needs stable concurrent sockets, new connection arrivals, a reconnect spike, or a long soak. Then name the message-level SLO, correlation strategy, server telemetry, and failure gate.

You should also be ready to explain why raw messages per second is incomplete, how session duration affects concurrency, why WebSocket and Socket.IO tests are not interchangeable, and how you prevent the load generator from becoming the bottleneck. The structured interviewQnA below provides concise model answers for those discussions.

Common Mistakes

  • Comparing fixed VUs with arrival rate as if they were identical. Fixed VUs cap active scenario workers. Arrival rate starts new sessions over time. Translate both into observed concurrent sockets before reading latency differences.
  • Counting frames instead of business outcomes. Heartbeats and subscription confirmations inflate received-message totals. Correlate a unique request ID with the exact acknowledgement or subscriber delivery that defines success.
  • Using k6/experimental/websockets in a new script. The 2026 module is k6/websockets. The experimental import is deprecated.
  • Testing Socket.IO as raw WebSocket. Socket.IO adds its own handshake, framing, namespaces, and events. A successful TCP upgrade does not mean the client follows the application protocol.
  • Closing immediately after the final send. The client may terminate before the reply is read. Wait for a matching response or allow a bounded drain interval.
  • Running the generator beside the service for capacity claims. Local tests are excellent for harness validation, but shared CPU and loopback networking distort capacity. Use isolated, monitored generators for serious results.
  • Ignoring generator saturation. Track generator CPU, memory, file descriptors, network throughput, and dropped iterations. A flat server graph with worsening client latency often points back to the injector.
  • Skipping thresholds. A report that looks reasonable can still violate a release SLO. Make the CLI exit nonzero when connection, delivery, or latency criteria fail.
  • Jumping from smoke to peak. Ramp gradually and watch server health. An instant maximum makes it harder to identify the first saturation point and can destabilize shared test environments.
  • Leaving idle connections out of the workload. Many production systems have far more connected listeners than active senders. Model both populations because memory and heartbeat costs remain even when message rate is low.

Long sessions can reveal leaks that a 50-second comparison misses. Use the long-running load test memory leak guide before declaring a WebSocket service production-ready.

Troubleshooting

ECONNREFUSED from both tools -> Run the Docker verification command, confirm port 10000, and check whether the container exited. If only one tool fails, inspect that tool's proxy environment and target URL.

The upgrade succeeds but zero messages return -> Check whether the endpoint expects a subprotocol, authentication frame, or different message schema. Turn on server logs. For Artillery, run DEBUG=ws npx artillery@2.0.33 run artillery-smoke.yml to inspect WebSocket activity.

k6 never finishes -> Ensure every connection has a bounded timeout and a close path for success, error, and silence. An open WebSocket keeps the VU iteration alive.

Artillery reports more received than sent -> The server may send a welcome frame, heartbeat, broadcast, or echoed data from other clients. Classify message types before using totals as delivery assertions.

Latency climbs while server utilization stays low -> Inspect generator event-loop delay, CPU, open files, DNS, TLS, and network limits. Repeat with multiple isolated generators before changing the application.

Local results vary sharply between runs -> Stop unrelated workloads, keep the same container image and tool versions, warm the service consistently, and run several repetitions. Report distributions and test conditions rather than selecting the best run.

Where To Go Next

Replace the echo payload with one real user journey, but keep the one-user verification stage. Then add a connection ramp, a steady hold, a reconnect spike, and a soak as separate scenarios so each answers one capacity question. The step-by-step k6 WebSocket tutorial goes deeper on k6 implementation.

Next, move the generator off the application host, export both client and server telemetry, and tag every run with service build, environment, tool version, and workload profile. If the system runs on Kubernetes, the cloud-native performance testing guide explains the surrounding infrastructure concerns.

Do not optimize for the largest number of sockets a laptop can display. Optimize for a reproducible point at which an application SLO changes, with enough evidence to identify the constrained resource.

Conclusion

For k6 vs artillery websocket load testing, k6 is the better default when you need precise concurrency control, message correlation, custom latency distributions, and detailed performance gates. Artillery is the better fit when declarative YAML, arrival-oriented journeys, and Node.js extensibility make the suite easier for the owning team to understand and maintain.

Run both smoke examples, choose the load model that matches production, and instrument business acknowledgements before increasing volume. The winning tool is the one that reproduces user behavior, fails on the right SLO, and produces evidence your team can act on.

Interview Questions and Answers

How would you choose between k6 and Artillery for a WebSocket project?

I would first define whether the workload needs fixed concurrent sockets, session arrivals, or both. I would choose k6 when message correlation, custom latency trends, tags, and threshold logic dominate. I would choose Artillery when readable YAML journeys and Node.js processor integration improve ownership. I would validate the choice with a representative pilot rather than a generic benchmark.

Why is messages per second insufficient for WebSocket performance testing?

It does not show how many connections succeeded, how long sessions stayed open, whether the correct recipient got a message, or how long acknowledgements took. Heartbeats and broadcasts can also inflate the count. I pair throughput with connection success, correlated delivery, latency percentiles, close codes, and server saturation signals.

How do closed and open workload models differ for WebSocket tests?

A closed model maintains a worker or VU population, so slow sessions can reduce new work. An open model starts sessions at a scheduled arrival rate even when earlier sessions are still running, which can expose queue growth. I select the model from production arrival behavior and verify actual concurrent connections at the server.

How would you measure WebSocket message round-trip latency?

I place a unique correlation ID and a local send timestamp in the message, then record elapsed time only when the matching acknowledgement arrives. I separate publisher acknowledgement from subscriber delivery for fan-out systems. I also count missing, duplicate, malformed, and late replies instead of recording latency only for successful messages.

How do you know whether the load generator is the bottleneck?

I monitor generator CPU, memory, network, file descriptors, event-loop health, and dropped work alongside application metrics. I repeat the run with additional isolated generators and compare the active-connection and latency curves. If capacity improves while the server profile stays similar, the original injector was likely saturated.

What thresholds would you set for a WebSocket release gate?

I set limits for failed upgrades, business message error rate, message acknowledgement p95 or p99, and unexpected close codes. I may also require a minimum completed message count so an empty test cannot pass. The numeric targets come from an agreed SLO and baseline, not from a tutorial value.

Why must Socket.IO and raw WebSocket use different test clients?

Socket.IO adds its own handshake, packet framing, events, namespaces, reconnection behavior, and acknowledgements above the transport. A raw WebSocket client can complete an upgrade without behaving like a Socket.IO user. I use a protocol-aware engine so the server executes the same code paths as production clients.

How would you design a WebSocket soak test?

I hold the expected mix of idle listeners and active senders for long enough to cross token refresh, heartbeat, cleanup, and deployment cycles. I track active connections, heap, garbage collection, file descriptors, broker lag, delivery latency, and reconnects over time. I use a low, sustainable message rate and define stop conditions that protect the environment.

Frequently Asked Questions

Is k6 or Artillery better for WebSocket load testing?

k6 is usually better for detailed WebSocket performance engineering because event handlers, custom metrics, thresholds, and executors provide precise control. Artillery is often better for teams that want concise YAML flows and arrival-rate scenarios. Choose based on the load model and required assertions, not syntax alone.

Can k6 test multiple WebSocket connections?

Yes. Each k6 VU can own a connection, and the current k6/websockets API uses a global event loop that can also support multiple concurrent connections inside one VU. Monitor generator resources and open file limits before scaling to high connection counts.

Does Artillery support raw WebSocket and Socket.IO?

Yes, but through different engines. Use the ws engine for raw WebSocket and the Socket.IO engine for Socket.IO framing, events, namespaces, and acknowledgements. Treating them as interchangeable produces misleading tests.

Which metrics matter in a WebSocket load test?

Track upgrade latency, successful open connections, current concurrent sessions, message send and receive counts, business acknowledgement errors, round-trip latency percentiles, session duration, and close codes. Correlate those client metrics with server CPU, memory, event-loop lag, file descriptors, queue depth, and downstream latency.

How do I compare Artillery arrival rate with k6 VUs?

Estimate steady concurrency as arrival rate multiplied by average session duration, then verify the estimate with server-side active-connection telemetry. A fixed VU count and an arrival rate are different models, so match their observed connection curves before comparing latency or throughput.

Can Artillery measure WebSocket message response time?

The built-in WebSocket engine reports sent and received counts plus send and receive rates. Basic YAML does not automatically correlate a sent application message with its matching response-time sample. Add protocol-aware custom code or server-side tracing when message round-trip percentiles are a core SLO.

Should I use k6/ws or k6/websockets in 2026?

Use k6/websockets for new scripts. It follows the standard WebSocket event style and uses a global event loop, while k6/ws is the older API with a local event loop. The k6/experimental/websockets import is deprecated.

How many WebSocket connections can one load generator create?

There is no trustworthy universal number because payload rate, TLS, compression, timers, operating-system limits, CPU, memory, and tool configuration all affect capacity. Increase load while monitoring generator saturation, then distribute the test before the injector becomes the limiting resource.

Related Guides