Memaknai hasil load test dengan benar: membaca metrik utama seperti jumlah VU, latensi, error, dan persentil p95 serta p99, lalu mengoptimalkan script k6 dengan http.batch, discardResponseBodies, dan expectedStatuses, serta menyusun threshold yang menargetkan SLA atau SLO.

Setelah episode 12, tes kalian aman, jujur, dan bisa dijalankan tanpa membuat tim infra marah. Tapi menjalankan tes hanyalah setengah cerita. Pertanyaan sesungguhnya muncul setelah angka muncul di layar: apa arti semua angka ini, dan apakah aplikasi kalian sehat?
Seorang dokter tidak mengobati pasien hanya dari satu angka tensi — ia melihat denyut, suhu, kadar oksigen, dan pergerakannya dalam waktu. Demikian pula load test: satu angka rata-rata bisa menipu. Episode 13 ini membahas dua sisi yang saling mendukung: bagaimana membaca hasil (metrik mana yang penting, dan mengapa p95 lebih jujur daripada rata-rata), dan bagaimana membuat script tidak bohong — karena mesin tes yang terlalu sibuk dengan JavaScript akan menghasilkan data yang justru mengukur k6, bukan aplikasi kalian.
Ringkasan akhir k6 (end-of-test summary) berisi deretan metrik yang sering membuat pemula kebingungan. Berikut yang paling penting untuk dijadikan teman:
| Metrik | Isi | Yang harus kalian pikirkan |
|---|---|---|
vus | jumlah virtual user aktif | apakah mendekati target yang direncanakan |
http_req_duration | durasi total satu request | metrik latensi utama, lihat avg, med, p(90), p(95) |
http_reqs | jumlah total request + rate per detik | apakah throughput memenuhi target |
http_req_failed | proporsi request gagal (status 4xx/5xx, koneksi error) | error rate yang disepakati dalam SLO |
http_req_blocked | waktu menunggu slot untuk membuka koneksi | membengkak jika koneksi dibuka ulang terus |
http_req_connecting | durasi TCP + TLS handshake | mengukur overhead jaringan, bukan aplikasi |
Metrik http_req_duration adalah bintangnya, dan di dalamnya k6 menampilkan beberapa statistik sekaligus: avg, med, min, max, p(90), p(95). Contoh ringkasan:
Perhatikan betapa jujurnya baris pertama: rata-rata 148.2 ms, median 120.5 ms, tapi ada lonjakan hingga 1.42 s, dan 5 persen request memakan lebih dari 520.9 ms. Kalau kalian hanya melihat rata-rata, kalian menyimpulkan aplikasi sehat. Persentil membongkar kenyataan: ada sebagian pengguna yang mengalami pengalaman jauh lebih buruk.
Ini pelajaran statistik yang paling penting dalam performance testing. Rata-rata bisa menyembunyikan ekor yang panjang: 99 request selesai dalam 100 ms dan satu request selesai dalam 10 detik menghasilkan rata-rata ~200 ms — terlihat baik, padahal ada pengguna yang menunggu 10 detik.
Persentil menceritakan kisah yang berbeda:
p(95) — 95 persen request selesai dalam nilai ini. 5 persen sisanya, entah seberapa lama, tidak ikut dalam angka ini.p(99) — 99 persen request selesai dalam nilai ini. Ini yang mendekati pengalaman pengguna paling tidak beruntung.Konsistensi persentil adalah indikator stabilitas yang jauh lebih kuat. Jika p(95) stabil sementara avg naik, mungkin ada beberapa outlier yang tidak mewakili mayoritas. Jika p(95) melonjak, mayoritas pengguna benar-benar terdampak. Inilah mengapa SLA yang baik berbunyi "95 persen request selesai di bawah 300 ms", bukan "rata-rata request di bawah 300 ms".
k6 mengeksekusi JavaScript di atas Go runtime (Sobek). Setiap pekerjaan yang tidak perlu yang dilakukan script adalah biaya yang tercampur ke metrik — dan jika mesin tes kehabisan sumber daya, tes mengukur mesin tes, bukan aplikasi. Empat teknik paling berdampak:
1. Kurangi overhead JavaScript. Pindahkan komputasi yang bisa dihitung sekali ke setup() atau scope modul. Parsing JSON berulang-ulang untuk data yang sama, atau membangun objek header baru di setiap iterasi, adalah sampah yang memperlambat VU. Hitung sekali, simpan, pakai ulang.
2. Pakai http.batch() untuk request paralel. Jika sebuah halaman memuat beberapa endpoint yang independen, jangan panggil satu per satu secara berurutan — itu memperpanjang durasi iterasi secara artifisial. http.batch() mengirimnya paralel dalam satu iterasi:
import http from "k6/http";
import { check } from "k6";
const responses = http.batch([
["GET", "https://api.example.com/v1/user"],
["GET", "https://api.example.com/v1/orders"],
["GET", "https://api.example.com/v1/notifications"],
]);
for (const [nama, res] of [
["user", responses[0]],
["orders", responses[1]],
["notifications", responses[2]],
]) {
check(res, {
[`${nama} merespons 200`]: (r) => r.status === 200,
});
}http.batch() sekaligus mengurangi overhead: request dikirim bersamaan dan hasilnya dikembalikan sebagai array. Default batch memungkinkan 20 koneksi paralel per panggilan, cukup untuk hampir semua kasus.
3. Aktifkan discardResponseBodies. Ini optimasi dengan dampak paling besar dan usaha paling kecil. Jika kalian tidak perlu membaca body response, k6 masih menyimpannya ke memori — dan mendorong GC bekerja lebih keras. Matikan dengan satu opsi:
export const options = {
vus: 50,
duration: "5m",
discardResponseBodies: true,
};Untuk request yang memang butuh body-nya, set responseType: "text" per-request. Hasilnya: memori tes turun drastis, GC jarang dipicu, dan metrik latensi tidak terkontaminasi oleh kerja tambahan mesin tes.
4. Gunakan http.setResponseCallback. Secara default k6 menganggap status 2xx dan 3xx sebagai sukses. Jika aplikasi kalian punya kode status sukses khusus (misal 202 Accepted untuk async job), set callback di level opsi:
import http from "k6/http";
export const options = {
vus: 50,
duration: "5m",
setResponseCallback: http.expectedStatuses(200, 201, 202),
};Dengan ini http_req_failed hanya menghitung status di luar daftar — metrik error menjadi akurat, dan threshold yang kalian susun berdiri di atas fondasi yang benar.
Angka yang bagus tidak ada artinya tanpa batas yang disepakati. Threshold adalah garis batas yang membuat k6 gagal secara eksplisit ketika aplikasi melanggar target — ini yang mengubah load test dari "melihat-lihat angka" menjadi "pemeriksaan penerimaan". Susun threshold berdasarkan SLO kalian:
export const options = {
vus: 100,
duration: "10m",
thresholds: {
http_req_duration: ["p(95)<300", "p(99)<800"],
http_req_failed: ["rate<0.01"],
http_reqs: ["rate>50"],
},
};"p(95)<300" — 95 persen request selesai di bawah 300 ms: SLO latensi utama."p(99)<800" — 99 persen di bawah 800 ms: pengaman untuk pengguna paling tidak beruntung."rate<0.01" — error rate di bawah 1 persen."rate>50" — throughput minimum 50 request per detik.Jika threshold dilanggar, k6 keluar dengan exit code non-nol — dan CI/CD bisa langsung menolak rilis. Untuk beban ekstra, tambahkan { abortOnFail: true, delayAbortEval: "30s" } agar tes berhenti lebih awal ketika threshold sudah pasti gagal, tanpa menunggu durasi penuh.
avg — sembunyikan ekor panjang. Lihat p(95) dan p(99).http_req_failed — error 1 persen dari 100 ribu request adalah 1.000 pengguna yang gagal.Pada episode 13 ini kalian telah mampu membaca hasil tes seperti seorang profesional: memahami metrik utama (vus, http_req_duration, http_reqs, http_req_failed, http_req_blocked, http_req_connecting), melihat kenapa persentil p95 dan p99 lebih jujur daripada rata-rata, mengoptimalkan script dengan pengurangan overhead JavaScript, http.batch(), discardResponseBodies, dan http.setResponseCallback(http.expectedStatuses(...)), serta menyusun threshold yang menargetkan SLA dan SLO — lengkap dengan abortOnFail untuk penghentian dini.
Inti yang harus kalian bawa:
p(95) dan p(99), jangan hanya avg.discardResponseBodies: true untuk tes membaca-bukan-mengolah body.Ringkasan terminal sudah cukup untuk satu tes. Tapi bagaimana kalau kalian menjalankan sepuluh tes per hari, ingin melihat tren mingguan, dan berbagi dashboard dengan tim? Itu butuh observability yang sesungguhnya. Di episode 14 kita akan membahas Observability, Tracing & Monitoring — mengintegrasikan k6 dengan Grafana, InfluxDB, dan Prometheus, memakai output eksternal, serta custom metrics dan tagging untuk analisis yang lebih dalam. Sampai jumpa di episode 14!