Writing your first complete k6 script: the basic structure with import, options, and export default function, running HTTP GET and POST requests, validating responses with checks, breaking scenarios into groups, then reading the k6 run output in the terminal.

In episode 2 you held the map of k6's architecture: the Go engine with goja, the virtual users and iterations model, the init-setup-default-teardown lifecycle, plus the checks, thresholds, and metrics components. Now it's time to fill in that map with your own handwriting. Episode 3 is the first hands-on episode: we write a k6 script that actually runs, from scratch.
This episode's goal is simple but fundamental: understand the mandatory structure of a k6 script (import, options, export default function, check), run HTTP GET and POST requests, validate responses, display output to the terminal, and use group() so complex scenarios stay readable. By the end of the episode, you'll run k6 run script.js yourself and read its output with confidence.
Every k6 script has four standard parts that always appear. Think of this as the body's skeleton: without any one part, the script doesn't function as it should.
k6/http, check, group, sleep).import http from "k6/http";
import { check } from "k6";
export const options = {
vus: 5,
duration: "10s",
};
export default function () {
const res = http.get("https://test.k6.io");
check(res, { "status is 200": (r) => r.status === 200 });
}Notice two things. First, export const options defines the load behavior — in this example, 5 VUs for 10 seconds. Second, export default function () is the heart of the script: this function is called repeatedly by each VU, and the requests and validations happen inside it. Code outside these two parts only runs during the init phase (episode 2).
Let's write your first file, smoke-get.js, in a practice directory:
import http from "k6/http";
import { check } from "k6";
export const options = {
vus: 1,
duration: "5s",
};
export default function () {
const res = http.get("https://test.k6.io");
check(res, {
"status is 200": (r) => r.status === 200,
"body contains welcome": (r) => r.body.includes("Welcome"),
});
}k6 run smoke-get.jsWhen it finishes, the terminal shows a summary — a final results table with metrics like http_req_duration and http_reqs, plus a checks block showing how many checks passed and failed. The output below is the shape you'll see (numbers simplified):
The two most important parts for this episode: the checks: 100.00% line showing both checks passed, and the http_reqs: 9 1.8/s line showing 9 requests were successfully sent in 5 seconds — roughly 1.8 requests per second with one VU.
Tip
Use k6's official practice site like https://test.k6.io for your early experiments. It is designed for exactly this and won't complain when hit with dozens of requests. Never start practicing against your company's production API — episode 13 will cover testing ethics and security in particular.
Next, a POST request that sends data. This is the most basic form of modern API interaction: send JSON, receive a response, validate. Note three important things: the Content-Type header, the JSON body serialized with JSON.stringify, and the third http.post parameter that accepts a params object.
import http from "k6/http";
import { check } from "k6";
export default function () {
const payload = JSON.stringify({
name: "Arman",
email: "arman@example.com",
});
const params = {
headers: { "Content-Type": "application/json" },
};
const res = http.post("https://httpbin.test.k6.io/post", payload, params);
check(res, {
"status is 200": (r) => r.status === 200,
"email echo matches": (r) => r.json().json.email === "arman@example.com",
});
}Here r.json() parses the response body into a JavaScript object, then we reach through the json field wrapping the payload to read the email. r.json() is the bridge between the response text and the structured data you can assert against.
To see details while the script runs, k6 provides console.log. Very useful when debugging scripts early on:
import http from "k6/http";
export default function () {
const res = http.get("https://test.k6.io");
console.log(`Status: ${res.status}`);
console.log(`Durasi: ${res.timings.duration} ms`);
console.log(`Panjang body: ${res.body.length}`);
}console.log output appears in the terminal as iterations run, with a timestamp prefix. Use it sparingly: at high load, thousands of iterations mean thousands of log lines flooding the terminal. For light debugging, this is the right tool; for production observation, leave it to metrics (episode 15).
When a scenario grows — login, fetch data, take an action, logout — a single flow becomes hard to read and its results hard to analyze. This is where group() plays its role: it breaks the scenario into named sections, and k6 reports the duration of each section separately in the output. It works like chapters in a book: a narrative structure that makes the story easier to follow.
import http from "k6/http";
import { check, group, sleep } from "k6";
export const options = {
vus: 2,
duration: "10s",
};
export default function () {
group("halaman beranda", () => {
const res = http.get("https://test.k6.io");
check(res, { "beranda 200": (r) => r.status === 200 });
});
group("halaman login", () => {
const res = http.get("https://test.k6.io/login");
check(res, { "login 200": (r) => r.status === 200 });
});
sleep(1);
}Note the sleep(1) at the end of the function — k6 loads 2 VUs for 10 seconds, and each iteration inserts a 1-second pause so the simulation is more realistic. Without a pause, k6 fires requests as fast as possible; with a pause, the behavior approaches that of real users who need thinking time between pages. This is a standard practice that distinguishes a test that "hammers" from a test that "simulates".
In the output, k6 displays the duration of each group in blocks named after your group names — █ halaman beranda and █ halaman login — complete with per-section check details. If one section is slower than the others, the numbers in the group block show exactly which section is the bottleneck — a big, often underestimated value of group().
export the options. If options isn't exported, k6 runs the test without VU configuration — the results are often confusing. Make sure it's export const options.export default, k6 refuses to run the script with a clear message. The function name must be default.import _ from "lodash" fails completely — k6 is not Node.js (episode 2). Import only k6 modules and local files.sleep().http_req_duration, http_reqs, and the checks block every time you finish.In this episode 3 you've written and run your first k6 script:
import, export const options, export default function, and check.JSON.stringify, setting the Content-Type header, and reading responses with r.json().console.log for quick, sparing debugging.k6 run script.js and the ability to read the summary, the checks block, and the per-group duration.You now write scripts that actually run. In episode 4 we raise the level with full focus on basic HTTP API load testing: authentication headers (bearer token), cookies, request parameterization with dynamic data, JSON payloads, combined functional and performance assertions with check(), plus understanding http.get, http.post, r.json(), and content-type. From scripts that run, we move to scripts that are professional. See you in episode 4!