k6 for API performance testing: scripting and metrics that matter

Plenty of Australian engineering teams discover their REST endpoints behave well in the Sydney lab and only stumble once traffic ramps up across regions that stretch from Brisbane to Perth. Latency variability introduced by residential NBN connections, transcontinental peering, and seasonal load spikes around events like EOFY sales or Boxing Day can turn a "fast enough" service into a queue of timed-out requests. k6 fits neatly into that reality because the same script that runs in a developer's local terminal can be piped into a CI pipeline that simulates the kinds of synthetic users a Canberra-based bank or a Melbourne marketplace sees on a weekday afternoon.

The toolset is also friendly for engineers who have been weaned on JavaScript thanks to the broader web platform rather than heavyweight test runners. A single binary, a script written in plain ES6, and a JSON-friendly output pipeline mean less ceremony for teams who already spend their day wrangling Atlassian-style monorepos. The price tag matters too: an open-source toolchain avoids the per-VU metering that some SaaS competitors levy, which is welcome news when budgeting against the Australian dollar at the end of a quarter.

Choosing k6 over the other load generators worth considering

Before digging into scripts, it helps to see where k6 sits next to the tools most engineers encounter during job interviews in Sydney or Brisbane. The short version: k6 trades the visual scene of JMeter for a code-first workflow that aligns with how modern backend services are already versioned and reviewed.

Tool Scripting language Learning curve Output format CI friendliness Licensing
k6 JavaScript (ES6) Low to medium JSON, CSV, Kafka, InfluxDB Excellent Open source (AGPL) + paid cloud
JMeter GUI + XML Steep JTL, CSV, XML Moderate Apache 2.0
Gatling Scala / Java / Kotlin Medium HTML, JSON, Feeder Good Apache 2.0 + paid enterprise
Locust Python Low CSV, custom Good MIT
Artillery JavaScript / YAML Low JSON, custom Good MPL-2.0 + paid cloud

What this comparison hints at, and what becomes obvious after running a few dozen test runs, is that k6's metric format was designed for the same observability world the service already lives in. The same Trend and Counter abstractions k6 uses internally can be pushed to Grafana, Datadog, or a self-hosted Prometheus stack without translation layers. That continuity matters in a market like Australia, where a lean platform team often owns both the load generator and the dashboard consumed by the on-call rota.

Scripting a meaningful k6 test for a REST endpoint

Once the choice is made, a script starts with a small handful of imports and a default function that describes what a single virtual user does during one iteration. For a fintech-style endpoint serving customer balances, the structure looks tidy: read a token from an environment variable, send an authenticated GET, capture the response time, and decide whether the call counts as a success.

import http from 'k6/http';
import { check, sleep } from 'k6';

const BASE_URL = __ENV.BASE_URL || 'https://api.example.com.au';
const TOKEN = __ENV.TOKEN;

export const options = {
  vus: 50,
  duration: '2m',
  thresholds: {
    http_req_duration: ['p(95)<400', 'p(99)<800'],
    http_req_failed: ['rate<0.01'],
  },
};

export default function () {
  const res = http.get(`${BASE_URL}/v1/accounts/me`, {
    headers: { Authorization: `Bearer ${TOKEN}` },
  });
  check(res, {
    'status is 200': (r) => r.status === 200,
    'latency under 350ms': (r) => r.timings.duration < 350,
  });
  sleep(Math.random() * 2 + 1);
}

The thresholds clause is where the script gains teeth. Instead of producing a report that says only "the test ran", the script enforces a pass-or-fail gate that maps directly to a service-level objective. If p95 crosses 400 milliseconds, k6 exits with a non-zero status, which is exactly what a Bitbucket Pipeline or a GitHub Actions runner expects from a quality gate. Engineers in Adelaide and Hobart have started wiring those gates into merge checks so a slow regression cannot quietly slip into the trunk on a Friday arvo.

A useful habit is to keep environment-specific values out of the script body. Hitting a .com.au domain during local smoke tests and a staging URL during CI avoids the "works on my machine" conversation that often eats a sprint of capacity. Pair the script with a small config.js or env vars and you have a single artefact that travels from a developer laptop in Parramatta to a runner hosted in the AWS Sydney region without modification.

Scenarios that match real Australian traffic shapes

A flat 50 virtual users over two minutes is rarely how production actually feels. Real traffic looks like a steep climb as the morning hits, a plateau through the middle of the day, and a tapering curve as the evening winds down across AEST. k6 captures that nuance through scenarios, and that is where the tool starts to feel less like a stress gun and more like a traffic choreographer.

A common pattern for an e-commerce backend is to combine an arrival-rate executor with a ramping-vus executor. The arrival-rate executor schedules requests at a fixed number per second, which is closer to what an API gateway actually queues, while the ramping executor simulates a manual soak test during the lead-up to an EOFY promotion. Pairing the two inside one script produces a believable picture of how a queueing layer absorbs bursts without starving background workers.

It is also worth leaning on tags. The same exec function can be tagged with a scenario field so filters in the output can isolate one cohort from another. When reviewing results, engineers can ask "how did the mobile cohort in Victoria behave compared to the desktop cohort in New South Wales?" simply by selecting a tag, which keeps the report honest and avoids the trap of averaging away the worst-performing slice. Run output can then be sliced by carrier, by region, or by device class to mirror the segmentation already used in production observability.

Metrics worth watching and the ones that mislead you

Default k6 metrics include http_req_duration, http_req_waiting, http_req_connecting, http_req_tls_handshaking, and iteration_duration. They look similar but tell very different stories. http_req_duration is the wall-clock time a request spends in flight, while http_req_waiting reveals server-side processing and queueing. When the gap between the two widens, the bottleneck usually sits inside the application, and when the gap shrinks, the network or TLS handshake deserves a closer look.

Custom Counter and Trend metrics allow a team to surface business-level signals such as the time a redemption endpoint spends inside a third-party fraud check or how often a particular error code fires under load. The trick is to keep the custom set small and meaningful; instrumenting every code path with a counter eventually buries the signal in noise. A pragmatic rule of thumb used by several Sydney platform teams is to limit custom metrics to those that already appear in an SLO document, which keeps the load test output aligned with the same dashboards on-call engineers stare at during an incident.

Percentiles deserve attention too. Reporting only an average hides the long tail that customers notice, particularly around p95 and p99. A handful of Tasmanian or far-north-Queensland users on slower NBN plans will see responses that look nothing like a mean, and ignoring those tails leads to a service that feels sluggish to the very customers a business depends on for organic growth.

Practical pointers for a first k6 engagement

Lining up a first serious run is mostly about discipline rather than tooling. Decide which questions the test needs to answer before writing a single line of JavaScript, then keep the script small enough that a teammate can review it during a morning stand-up. The paragraphs below describe what usually trips newcomers, after which the recommendations list summarises the habits worth keeping.

The most common pitfall is overloading the first run with exotic scenarios. A clean smoke pass against one endpoint, watched from a single dashboard, produces more value than a sprawling multi-stage script that confuses reviewers. Once the smoke pass is green, layer on arrival-rate, soak, and stress flavours in that order; each new flavour should answer one new question and produce its own threshold file.

Once a baseline is established, treat the thresholds as living artefacts reviewed every quarter alongside the SLOs they reference. Gradual drift in p99 is easier to manage when the alert that surfaces it sits in the same chat channel as the incident that triggered it. That symmetry between load tests and operations is what turns a project from "we ran a benchmark" into a feedback loop the engineering org actually trusts.

The clearest thing to take away from a first engagement is that k6 pays off when the script is short enough to read in one sitting, when the thresholds mirror the SLOs the team already holds itself to, and when the output lands in the same observability pipeline everything else the team watches. With those three habits in place, performance testing stops being a special event that someone volunteers for late in the project and becomes another quiet guardrail around every deployment that leaves the trunk.