Module 1: Why Load And Performance Testing

5. What k6 is and its runtime

Overview

You already know what questions a load test answers and what types exist; time to meet the tool the industry runs them with. It's called k6: an open-source load generator, maintained by Grafana Labs, built for developers. Its central idea —and the one that confuses most people— is this: you write your tests in JavaScript, but k6 is not Node. k6 is a program written in the Go language that carries inside its own JavaScript engine; when you run k6 run script.js, it's not Node that runs your file, but that engine embedded in k6. It sounds like a technical detail, but it has very practical consequences: there's no npm install, no node_modules, Node's libraries and its APIs (fs, require, express) don't work, and in exchange you get a single, blazing-fast binary capable of simulating thousands of users. In this lesson we understand what k6 is, why it runs on its own runtime, what you can and can't do because of that, and why in this guide its scripts are presented as labeled content (k6 isn't installed in this environment).

Connection to the module: this lesson introduces the tool; the anatomy of a k6 script —the default function, http.get/post, check(), sleep(), export const options— is the whole of module 2. Here we stay at the level of "what kind of thing k6 is and how it relates to what you already know (Node, npm, Python)." In lesson 7 you'll see your first real k6 script (as content) hitting Reservo, and its executable sibling in Python. Think of this lesson as introducing the actor before seeing it perform.

The appliance that speaks your language but isn't your brand

Imagine an industrial mixer programmed with recipes written in English —"mix 2 minutes, rest 30 seconds, mix 1 minute"—. You already know English, so writing the recipe feels natural. But the mixer isn't your home kitchen: you can't plug in your home blender's attachments, it doesn't accept the ingredients from your personal pantry, and it doesn't have your microwave's buttons. It speaks your language (English), but it's a different machine, with its own motor and its own parts, designed for one thing: mixing at industrial scale, tons of dough, without overheating.

k6 is that mixer. The "language" is JavaScript: you write your tests in JS because it's a language many developers already know, and that lowers the barrier to entry. But the "machine" is not Node.js, your everyday JavaScript environment. It's a different engine, embedded inside a program written in Go, specialized in one thing: generating load at scale —thousands of virtual users— without choking. That's why you can write your tests with the comfort of a familiar language, but you can't bring "the attachments from home": npm libraries, Node modules, the file-system APIs. Understanding this dual nature —familiar language, specialized machine— is the key to not getting frustrated when require('fs') doesn't work.

What k6 is, concretely

k6 is an open-source tool for load and performance testing. Its defining traits:

  • It's a single binary, written in Go. You install it as a single executable (k6), with no runtime environment around it. It drags no dependencies: you download the binary and it runs. Go is a language built for concurrency, and that's why k6 can simulate thousands of virtual users with modest resource consumption —much more efficient than if each virtual user were a heavy process—.
  • Tests are written in JavaScript. A k6 script is a .js file. JavaScript was chosen because it's familiar to a huge number of developers; writing a load test resembles writing any JS script, with import, functions, and objects.
  • It's oriented to developers and to "testing as code." The test is code: it's versioned in git, reviewed in a pull request, run in CI. It's not a clicks-and-menus tool; it's a text file that lives next to your code, a philosophy it shares with pytest and Playwright.
  • It's run from the terminal. The central command is k6 run script.js, which runs the script with the configured load and, at the end, prints a summary with the metrics (latency, throughput, errors) you already know from lesson 3.

In one sentence: k6 is a Go binary that runs JavaScript scripts to launch traffic against your system and measure how it responds under load.

The own runtime: what "it's not Node" means

Here's the point to internalize, because it explains almost all the surprises of a k6 beginner. When you write JavaScript, your instinct (if you come from the web world) says "this runs on Node." With k6, no. k6 carries its own JavaScript engine embedded —an interpreter written in Go— that runs your script. Node never intervenes. Your .js file is read and run by k6, not by node.

This has very concrete consequences:

  • There's no npm or node_modules. You don't install dependencies with npm install. The modules you use come inside k6: k6/http to make HTTP requests, check and sleep from the k6 module, k6/metrics for custom metrics. They're modules the binary already ships.
  • Node's APIs don't work. No require('fs') to read files as in Node, nor require in general —k6 uses ES modules (import ... from ...), not Node's CommonJS require—. There's also no process, no Node http, and no frameworks like Express. If you copy a Node snippet that uses those APIs, it won't run in k6.
  • You don't run your JavaScript app inside k6. k6 isn't for running your Node server; it's for hitting it from outside. The k6 script is the client that generates load, not the application under test.
  • In exchange, you gain scale and simplicity. A single binary, without installing a whole ecosystem, capable of generating massive load efficiently. That's the deal: you give up Node's libraries and get a specialized, fast load generator.

To place it against what you already know in the Testing ecosystem:

Node.jsk6
What it isA general-purpose JavaScript runtimeA load generator (Go binary) that runs JS
JS engineV8 (Chrome's)Its own engine embedded in Go
Installing dependenciesnpm install, node_modulesNo; the modules (k6/http) come inside
Available APIsfs, http, process, all of npmOnly k6 modules (k6/http, check, sleep...)
What it's forRunning apps, servers, JS toolsOnly: generating load and measuring
How it's invokednode app.jsk6 run script.js

The mental rule: k6 uses JavaScript as a language, not as a platform. The language is familiar; the platform is its own.

What a k6 script looks like (content)

To make the idea land, here's the skeleton of a minimal k6 script. Important label: this block is CONTENT, not a run of this environment —k6 isn't installed here—. It's correct and faithful to k6's official documentation; we take its anatomy apart in module 2. Look at it only to recognize the shape:

// CONTENT (not run here): a minimal k6 script. See grafana.com/docs/k6
// The modules come INSIDE k6 (not from npm): 'k6/http' and 'k6'.
import http from "k6/http";
import { check, sleep } from "k6";

// The 'default' function is the code each virtual user (VU) runs in a loop.
export default function () {
  const res = http.get("http://localhost:8000/rooms");

  // check() verifies correctness UNDER load (it doesn't stop the test if it fails).
  check(res, {
    "status is 200": (r) => r.status === 200,
  });

  sleep(1); // "think time": a pause like a real user between actions.
}

Notice three things that give away that it's not Node: the imports are from "k6/http" and "k6" —modules the binary ships, not npm packages—; there's no require anywhere; and there's no package.json or node_modules accompanying this file. It's a single .js that k6 knows how to run on its own. If you tried to run it with node script.js, it'd fail —Node doesn't know "k6/http"—; and the other way around, k6 wouldn't run a script that uses require('express'). Each one speaks its own dialect.

And this is what the command and the start of its output would look like —also content, not run here—, so you recognize the interface:

// CONTENT (not run here): shape of the invocation and the start of k6 run
$ k6 run script.js

     execution: local
        script: script.js

     scenarios: (100.00%) 1 scenario, 1 max VUs, 10m0s max duration
              * default: 1 iterations shared among 1 VUs

The full summary with the metrics (http_req_duration, p95, checks) we'll see in lesson 7 and, in depth, in module 3. For now it's enough to recognize: k6 run, a JS script with k6 modules, and a summary at the end.

Why in this guide k6 is content (and why that's fine)

Since the environment where this guide was prepared doesn't have k6 installed —it's a separate Go binary, and we don't take it for granted—, all k6 scripts and outputs are presented as labeled content: correct and faithful to the official documentation, but never disguised as "I ran it and this came out." This honesty matters: in performance testing, presenting made-up numbers as measured is exactly the sin the discipline fights. We prefer to say "this is how k6 looks" and, for the concepts (latency, percentiles, throughput, breaking point), actually run a mini generator in Python that does conceptually the same thing at a smaller scale. That way you see real numbers even though the k6 runner is content. If you install k6 on your machine (with a package manager or by downloading the binary), these same scripts will run as-is; they're written to be executed, not just read.

Common mistakes

Trying npm install or require() in a k6 script. What happens: someone wants to use an npm library (for example, a dates one) inside their test and does require('some-npm-lib'); k6 fails. Why it happens: it's assumed k6 is Node. How to detect it: if your script uses require, process, fs, or an npm package, it won't run in k6. How to fix it: use the modules k6 ships (k6/http, check, sleep, k6/metrics) and import of ES modules. (There are advanced ways to bundle dependencies with a bundler, but that's specialized territory; 95% of tests are written with only k6's modules.)

Confusing "the app under test" with "the k6 script." What happens: someone thinks they have to put their server inside the k6 script, or that k6 "runs their app." Why it happens: since both are JavaScript, the roles get mixed. How to detect it: if your k6 script tries to be the server instead of hitting it, you inverted the roles. How to fix it: remember k6 is the client that generates load from outside; your application (Reservo, your API) runs separately, and k6 hits it with http.get/http.post. They're two different programs.

Trying to run the k6 script with node. What happens: someone writes node script.js expecting their k6 test to run, and gets an error about "k6/http" not being found. Why it happens: the file is .js, so it seems Node should run it. How to detect it: if you invoke node on a file that imports from "k6/...", you're using the wrong machine. How to fix it: k6 scripts are run with k6 run script.js, never with node. The language is JavaScript; the interpreter is k6.

Exercises

Exercise 1 — Node or k6? For each statement, say whether it describes Node.js, k6, or both. (a) "It runs JavaScript code." (b) "Dependencies are installed with npm install and node_modules." (c) "Its purpose is to generate load against a system and measure its performance." (d) "Modules like the HTTP client come inside the binary, not from a package manager." (e) "It's run with k6 run."

See solution
  • (a) Both. Both run JavaScript —Node with V8, k6 with its own engine in Go—.
  • (b) Node. npm install and node_modules are Node's; k6 doesn't use them.
  • (c) k6. Generating load and measuring performance is k6's specific purpose; Node is general-purpose.
  • (d) k6. In k6, k6/http and the rest come inside the binary; in Node, the HTTP client is either imported from the core or installed from npm.
  • (e) k6. k6 run is k6's command; Node is invoked with node.

Exercise 2 — Diagnose the error. A colleague copied this snippet into a k6 script and it doesn't work:

const fs = require("fs");
const axios = require("axios");
const data = fs.readFileSync("rooms.json");

Explain in two or three sentences why none of those three lines run in k6, and what you'd conceptually replace it with to make an HTTP request.

See solution

None run because all three assume they're in Node, not k6: require doesn't exist in k6 (it uses import of ES modules), fs is a Node API k6 doesn't have, and axios is an npm package k6 can't install or use. To make an HTTP request in k6 you use its own module: import http from "k6/http" and then http.get(url) or http.post(url, body). (To read data from a file, k6 has its own mechanisms —like open() or SharedArray—, an advanced topic not covered here.)

Exercise 3 — Explain the trade-off. In your own words (two or three sentences), what does k6 lose by not being Node, and what does it gain in exchange? Why does that deal make sense for a tool whose only job is generating load?

See solution

k6 loses access to Node's ecosystem: it can't use the hundreds of thousands of npm packages, nor the system APIs (fs, process), nor frameworks like Express. In exchange it gains a single dependency-free binary, very efficient, capable of simulating thousands of concurrent virtual users with little resource consumption (thanks to Go being made for concurrency). The deal makes sense because k6's job is a single one —generate load at scale and measure— and for that it doesn't need Node's whole ecosystem; it needs speed, concurrency, and deployment simplicity, which is exactly what its own runtime gives it.

Summary and next step

In this lesson you met k6: an open-source load generator, a single binary written in Go, whose tests are written in JavaScript but run on their own runtime —not on Node—. You understood the central consequence of that: there's no npm or node_modules, Node's APIs (require, fs, process) and external packages don't work; instead you use the modules k6 ships inside (k6/http, check, sleep). In exchange you gain scale and simplicity: a single binary capable of simulating thousands of users. It's the mixer that speaks your language but isn't your brand.

The reason for the guide's rule is also clear: since k6 isn't installed in this environment, its scripts and output are presented as labeled content —correct and faithful to the official docs, never disguised as executed—, while the concepts are actually run with the Python generator. You already saw the shape of a script (import http from "k6/http", the default function, check, sleep) and of the command (k6 run script.js).

Before moving on you should be able to: explain why a k6 script doesn't run with node; name where k6's modules come from (the binary, not npm); and say in one sentence what k6 loses and gains by having its own runtime.

What comes next is to stop talking about the tool and bring up the target. In lesson 6 we build the entire canonical Reservo API in Python —GET /rooms, POST /quote, POST /book— and see it respond the anchor numbers (7500, 6000) with actually executed output. With no target there's no load to measure.

Resources