GR

Grafana k6

Developer-friendly load testing and performance tool

Monitoring & Status ★ 31.6k stars Medium setup AGPL-3.0

Grafana k6 is an open-source load testing tool that lets developers write performance tests in JavaScript. It is used for synthetic checks, regression testing, and validating system reliability under load.

Key features

  • JavaScript test scripting
  • Load and performance testing
  • CLI and CI integration
  • Metrics output to many backends

Pros & cons

Strengths

  • Developer-friendly scripting
  • Great CI integration

Trade-offs

  • Not a continuous monitor
  • Distributed runs need extra setup

Grafana k6 replaces

Last reviewed Aug 26, 2026 · 827 words

k6 will tell you in 90 seconds that your reverse proxy starts returning 502s at about 300 concurrent users. It will not tell you at 3 a.m. that it has actually happened. That distinction is the whole guide: k6 is a load-testing tool you run on purpose, before an upgrade or after a config change, and people who install it expecting an always-on health checker end up disappointed and with a very hot server.

It is a Go binary that runs JavaScript, not a Node program

k6 ships as a single Go executable (about 256 MB of RAM at idle, more as virtual users climb) and embeds its own JavaScript runtime. That means your test scripts are plain ES modules with import http from 'k6/http', but there is no npm install and no Node APIs; anything beyond the built-in modules has to be bundled first. In practice that is a feature. A test is one file, checked into the same repo as your compose stack, runnable from docker run --rm -i grafana/k6 run - < smoke.js on any box with Docker. The 31,000-star project is AGPL-3.0 and Grafana Labs maintains it, so the free tool and the paid Grafana Cloud k6 share the same scripts.

The 20-line script that pays for itself

Here is the test I run against anything I put behind a proxy, adjusted to a cheap endpoint the app itself exposes:

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

export const options = {
  stages: [
    { duration: '30s', target: 50 },
    { duration: '1m',  target: 200 },
    { duration: '30s', target: 0 },
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'],
    http_req_failed:   ['rate<0.01'],
  },
};

export default function () {
  const res = http.get('https://photos.example.com/api/server/ping');
  check(res, { 'status is 200': (r) => r.status === 200 });
  sleep(1);
}

Ramp to 200 virtual users over 90 seconds, each hitting the endpoint once a second. The two thresholds lines are the part worth copying: if the 95th-percentile response time crosses 500 ms or more than 1% of requests fail, k6 exits non-zero. A non-zero exit is what lets a load test sit in a CI job and block a deploy, which is the "great CI integration" that gets mentioned about k6 and is the reason it beat the GUI-driven tools it replaced.

What a self-hoster actually learns from a run

The interesting failures are never in the app. Three I have caught with scripts like the one above: a Traefik instance with default timeouts queueing requests behind a slow upstream until everything went 504; an app on SQLite where 40 concurrent writes locked the database and every read stalled behind them; and a nightly backup job that overlapped my test window and halved throughput, which is how I learned the backup was running 2 hours longer than I thought. None of those show up on a dashboard that only samples once a minute.

Test your own services only. Running this against a site you do not own is indistinguishable from an attack, and hosting providers treat it as one.

Results go to Prometheus or Grafana with one flag

The terminal summary is fine for a one-off. For anything you want to compare across weeks, ship the metrics out: k6 run -o experimental-prometheus-rw script.js with K6_PROMETHEUS_RW_SERVER_URL pointing at a Prometheus instance that has remote-write receiving enabled, then graph k6_http_req_duration_p95 in Grafana next to your CPU and container metrics. InfluxDB and JSON outputs exist too. Seeing request latency on the same time axis as the proxy's CPU is what turns "it got slow" into "it got slow because the ML container pegged all four cores".

The monitor you still need is a different tool

k6's own docs are careful about this: it is not a continuous monitor. For "is it up right now, tell my phone", you want Uptime Kuma or Gatus, which poll every 30 to 60 seconds with one request and cost nothing. The homelab monitoring post covers that side. k6 is the tool you run once a month, and once before anything scary.

Distributed runs are the other honest limit. One machine can drive a few thousand virtual users depending on script complexity; past that you need the k6-operator on Kubernetes or the paid cloud runner, and both are more setup than a homelab wants.

What I'd do

Keep a loadtests/ directory beside the compose file, one script per service, thresholds set to what you would actually accept. Run the set before every major upgrade and after any reverse-proxy change, with output to Prometheus so you can see the trend. Leave the always-on alerting to Uptime Kuma. Used that way, k6 is the cheapest insurance in the monitoring category: 20 lines of JavaScript and 90 seconds, and you know your limit before your users do.

Similar monitoring & status apps