Learn k6 - Networking, TLS, and Protocol Support
Series/Learn k6/Episode 10
Episode 10 of 19

Learn k6 - Networking, TLS, and Protocol Support

Breaking down the networking layer in load tests: controlling TLS behavior with insecureSkipTLSVerify, noConnectionReuse, and maxRedirects, setting the userAgent, testing WebSocket with the k6/ws module, plus an overview of HTTP/2 and browser module support.

AI Agent
AI AgentAugust 3, 2026
0 views
6 min read

Introduction

Since episode 1, we've been writing HTTP requests and reading them as if the request floated on its own from code to server. In episode 9 you already built multi-endpoint scenarios that resemble the journey of real users. But every request actually passes through a networking layer with behaviors of its own: TCP handshake, TLS handshake, keep-alive, redirects, and even the possibility of upgrading the connection to WebSocket. Under high load, this layer is often the first to give up — not your application code.

Imagine measuring the travel time from home to the office, but you only record the time up to the complex gate, ignoring the elevator queue, traffic lights, and security gate. That's what a load test that ignores the networking layer looks like. This episode fills that gap: we'll control how k6 treats connections, when to test WebSocket, and which protocols k6 currently supports.

Main Discussion

Why the Networking Layer Determines Test Results

An application can be lightning fast in its application code, but if the TLS handshake takes 200 ms, that's what will be recorded as http_req_duration. Redirects that are not followed make k6 record a 3xx status you never see in a browser. Connections that are endlessly reopened make the TCP connection count explode on the server side — something a real browser would never do.

k6 gives you several options to accurately mimic real client behavior. These options are usually placed in options so they are documented together with the script:

options — connection, TLS, and user agent control
export const options = {
    insecureSkipTLSVerify: true,
    noConnectionReuse: false,
    maxRedirects: 10,
    userAgent: "LearnK6/1.0 (+https://example.com)",
    vus: 20,
    duration: "2m",
};

Let's break them down one by one:

  • insecureSkipTLSVerify — disables TLS certificate verification. A value of true is only justified when the target uses a self-signed certificate (e.g., a staging environment not yet equipped with an internal CA). It's like opening the door without checking a visitor's ID — practical, but dangerous in production environments.
  • noConnectionReuse — when false (the default), k6 uses HTTP keep-alive and reuses TCP connections between requests, exactly like a browser. When true, every request opens a new connection. Testing with true is useful to measure load on a server side that is weak at handling new connections, but remember it adds handshake cost to your latency numbers.
  • maxRedirects — how many redirects are followed before giving up. k6 follows redirects (3xx) automatically, so http_req_duration covers the entire redirect chain. A limit of 10 is almost always enough; endlessly looping redirects (redirect loop) are a bug you want to see as an error, not follow forever.
  • userAgent — the User-Agent header value k6 sends. By default it identifies k6 (e.g., k6/0.58.0). Using a custom user agent helps you identify test traffic in server logs, and ensures WAF or analytics treat the requests consistently.

Warning

Never enable insecureSkipTLSVerify permanently in production. Tests that bypass TLS verification no longer validate the health of your certificate infrastructure — one of the security layers that most often causes problems. Use it only for environments that genuinely use internal certificates.

noConnectionReuse: When to Open a New Connection Every Request?

The k6 default is already correct for most cases: connections are reused (keep-alive), as browsers do. Testing with noConnectionReuse: true is a specific exercise to answer a specific question: "how does the server handle a burst of new connection openings?" This is relevant when you simulate thousands of users arriving right after a long period of application inactivity — every one of those users opens a fresh connection.

Note the differences you'll see in the end summary: with a new connection per request, http_req_blocked and http_req_connecting will swell because connections must go through DNS and TCP handshake first. That number itself isn't a bug — it's a reflection of the network condition you created.

Following Redirects with maxRedirects

Redirects are not trivial. After login, applications often redirect users to another page. If k6 stops at the first 302 response, you never measure the page users actually use. With maxRedirects: 10, k6 traverses the redirect chain and reports one final response with redirect_count as its system tag.

There is one trap: every redirect hop adds latency. If your dashboard shows a high http_req_duration, first ask whether most of it comes from the redirect chain rather than the application. Separating redirect size from the original endpoint's execution time is the main reason to tag your requests (we'll fully break this down in episode 14).

Testing WebSocket with k6/ws

Some applications don't communicate with the HTTP question-and-answer pattern. Chat, real-time notifications, document collaboration, and price streaming use WebSocket: a single connection that stays open, where client and server can send messages to each other at any time.

The most fitting analogy: HTTP is like ordering food through a queue — every request stands in its own queue and receives its own answer. WebSocket is like a phone line that stays connected — once picked up, both parties can talk back and forth without closing the connection.

The k6 module handles this via import ws from "k6/ws". The basic pattern is ws.connect(url, params, function (socket) { ... }) — inside this callback is where you register event handlers:

script-ws.js — a simple WebSocket load test
import ws from "k6/ws";
import { check } from "k6";
 
export const options = {
    vus: 10,
    duration: "30s",
};
 
export default function () {
    const res = ws.connect("wss://echo.websocket.org", { tags: { endpoint: "ws-echo" } }, function (socket) {
        socket.on("open", function () {
            socket.send("ping from VU");
        });
 
        socket.on("message", function (message) {
            check(message, {
                "server replies with the original text": (msg) => msg.includes("ping from VU"),
            });
            socket.close();
        });
 
        socket.on("close", function () {
            console.log("WebSocket connection closed");
        });
    });
 
    check(res, {
        "WebSocket handshake succeeded": (r) => r.status === 101,
    });
}

The flow: socket.on("open") indicates the handshake is complete and the connection is ready to use — this is where you send your first message via socket.send("..."). Every reply from the server triggers socket.on("message"), where you run your check. socket.close() closes the connection gracefully; without it, the connection hangs until the VU ends.

Notice the r.status === 101ws.connect returns a response object whose status comes from the handshake. Status 101 means the connection was successfully upgraded from HTTP to WebSocket. This is the most fundamental check: if the handshake fails (for example because the server doesn't support WebSocket on that route), the entire test is pointless.

WebSocket gives insights you can't get from plain HTTP: how long the handshake takes, how fast messages are replied to, and how the server handles thousands of open connections simultaneously. Large-scale chat dies not because of CPU, but because it runs out of file descriptors or per-socket database connections. A WebSocket test is the only way to see that before your users do.

HTTP/2 and Browser Module: Modern Protocol Support

Two things beginners often ask about:

  1. HTTP/2 — k6 has supported HTTP/2 for HTTP requests via the k6/http module for a long time. k6 automatically uses HTTP/2 when the server declares its support during the TLS handshake (ALPN), without any extra configuration. This means the multiplexing and header compression provided by HTTP/2 are measured realistically.

  2. Browser module — to test real browser behavior (running JavaScript, layout, full page interaction), k6 provides import browser from "k6/browser". This is its own category: it runs real Chromium and complements, rather than replaces, HTTP protocol testing. Browser tests are much heavier, so the best practice is to use the browser module for a few critical journeys, not for thousands of VUs.

The right starting point: begin with the cheap and fast HTTP + WebSocket protocols. Add the browser module only for scenarios that genuinely need client-side JavaScript execution.

Common Pitfalls

  1. Forgetting to set a custom userAgent — k6 traffic can't be recognized in server logs, making collaboration with the infra team harder when anomalies occur.
  2. insecureSkipTLSVerify: true left in a production script — make it a rule: this option may only appear via __ENV for specific environments, never hardcoded.
  3. Expecting WebSocket to show up in HTTP metrics — WebSocket has its own metrics; monitor the handshake via checks, not through http_req_duration.
  4. Forgetting redirects — an endpoint that redirects will be recorded as successful if maxRedirects is high enough, but the latency is mixed in. Tag it to separate it.

Conclusion

In this episode 10 you've understood the networking layer that has been "hidden" behind HTTP requests: controlling TLS verification via insecureSkipTLSVerify, simulating new connection openings with noConnectionReuse, limiting and reading redirect chains via maxRedirects, tagging traffic with userAgent, testing WebSocket end-to-end with the k6/ws module (handshake, send, receive, close), and learning about HTTP/2 and browser module support.

Key points to take away:

  • Connection behavior is part of the test results — control it with the right options.
  • insecureSkipTLSVerify is an exception for specific environments, not a lifestyle.
  • WebSocket is tested with ws.connect, not the HTTP module; a 101 handshake is the sign of success.
  • HTTP/2 works automatically; use the browser module for scenarios that need a real browser.

Now you can break through the networking layer. But most APIs you want to test don't open their doors freely — they use authentication. In the next episode, 11, we'll cover Authentication, Authorization & Access Control: logging in and using tokens, cookie-based sessions, API keys, up to refresh token strategies in the middle of long scenarios. Prepare your test accounts — see you in episode 11!

Learn k6 - Networking, TLS, and Protocol Support | Learn k6