Learn k6 - Writing Your First k6 Script
Series/Learn k6/Episode 3
Episode 3 of 19

Learn k6 - Writing Your First k6 Script

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.

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

Introduction

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.

The Mandatory Structure of a k6 Script

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.

  1. Import — pulls in the k6 modules you need (k6/http, check, group, sleep).
  2. Options — execution configuration (number of VUs, duration, thresholds).
  3. Default function — the main function called repeatedly by each VU; its contents are the load under test.
  4. Helper (optional) — supporting functions called from within the default function.
A complete k6 script skeleton
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).

Your First HTTP GET Request

Let's write your first file, smoke-get.js, in a practice directory:

smoke-get.js: a simple GET request
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"),
  });
}
Running the script
k6 run smoke-get.js

When 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):

Example k6 run output (abridged)
   checks.........................: 100.00% 2 out of 2
   data_received..................: 35 kB 7.0 kB/s
   data_sent......................: 1.4 kB 280 B/s
   http_req_blocked...............: avg=41.3ms  min=34.1ms  med=38.2ms
   http_req_connecting............: avg=10.3ms  min=9.4ms   med=10.1ms
   http_req_duration..............: avg=112ms   min=104ms   med=111ms
   http_req_failed................: 0.00%  0 out of 9
   http_reqs......................: 9 1.8/s
   iterations.....................: 9 1.8/s
   vus............................: 1 min=1 max=1
 
   ✓ status is 200
   ✓ body contains welcome
 
   █ init
 
   checks.........................: 100.00% ✓ 2  ✗ 0

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.

HTTP POST Request with a Body

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.

smoke-post.js: POST request with a JSON body
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.

Logging Output to the Terminal

To see details while the script runs, k6 provides console.log. Very useful when debugging scripts early on:

Logging with console.log
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).

Breaking Scenarios into Pieces with group()

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.

group(): breaking a scenario into named sections
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().

Common First-Script Mistakes

  1. Forgetting to 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.
  2. The default function isn't exported. Without export default, k6 refuses to run the script with a clear message. The function name must be default.
  3. Using npm modules. import _ from "lodash" fails completely — k6 is not Node.js (episode 2). Import only k6 modules and local files.
  4. Zero pause between requests. The load becomes unrealistic and often exceeds the target's real capacity. Always consider sleep().
  5. Writing a script without reading the output. The terminal summary is your primary source of information. Get in the habit of reading http_req_duration, http_reqs, and the checks block every time you finish.

Conclusion

In this episode 3 you've written and run your first k6 script:

  • Mandatory structure: import, export const options, export default function, and check.
  • HTTP GET and POST: sending requests, sending a JSON body with JSON.stringify, setting the Content-Type header, and reading responses with r.json().
  • Logging: console.log for quick, sparing debugging.
  • group(): breaking scenarios into named sections measured separately.
  • Execution: 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!

Learn k6 - Writing Your First k6 Script | Learn k6