Bringing load test results out of the terminal: sending data to InfluxDB, Prometheus, or a JSON file, displaying it on a Grafana dashboard, and using custom metrics and tagging for deeper analysis of every endpoint and scenario.

In episode 13 you learned to read the k6 end-of-test summary — p(95), p(99), error rate, throughput. But that summary is only a snapshot at the end of the test: a single point in time, no history. A real load test has a shape, not just a conclusion. When did latency start to degrade? Is VU number 50 the point where the system starts to tire? Questions like these can't be answered by a single terminal table — they need raw data that is stored and can be visualized.
Episode 14 closes the fundamental section of this series with observability: how to send k6 results to metric storage systems (InfluxDB, Prometheus, or a JSON file), display them on a Grafana dashboard that can be shared with the team, and enrich the data with custom metrics and tagging so the analysis is sharp, not just pretty. A terminal summary is a report; a dashboard is a story that keeps unfolding.
k6 displays a summary at the end, but that raw data can be sent to many destinations with the -o (output) flag. The -o flag can be stacked, so a single test can write to a file and a database at the same time. The three most important outputs to understand:
k6 run -o json=results.json script.jsk6 run -o influxdb=http://localhost:8086/k6 script.jsk6 run -o experimental-prometheus-rw=http://localhost:9090/api/v1/write script.jsjson=results.json — stores every raw data point. Useful for audit, post-processing, or combining results from several tests. This JSON file is the full truth; any dashboard is merely a way of reading that truth.influxdb=http://host/db — writes to an InfluxDB time-series database. The part after the host is the database name (in the example above: k6). This is the classic and very stable path for the k6 + Grafana pairing.experimental-prometheus-rw=http://host/api/v1/write — writes to Prometheus via remote write. This path has become the modern standard because Prometheus is the hub of many organizations' metric ecosystems.Tip
Grafana Cloud provides a built-in k6 integration: run tests from the cloud and the dashboard fills in automatically. For Datadog, k6 has no built-in output — use the surrounding ecosystem, for example the xk6 extension or forwarding results through a third-party aggregator. The pattern is always the same: test results are sent to an external destination, then visualized there.
The cheapest way to experience full observability is to run InfluxDB and Grafana locally with Docker Compose, then fire tests at that stack with k6. The full picture:
services:
influxdb:
image: influxdb:1.8
container_name: influxdb
ports:
- "8086:8086"
environment:
- INFLUXDB_DB=k6
volumes:
- influxdb-data:/var/lib/influxdb
grafana:
image: grafana/grafana:latest
container_name: grafana
ports:
- "3000:3000"
volumes:
- grafana-data:/var/lib/grafana
volumes:
influxdb-data:
grafana-data:Notice INFLUXDB_DB=k6 — this tells InfluxDB to create the k6 database on first boot, the exact database that -o influxdb=http://localhost:8086/k6 targets. Grafana only needs port 3000 and a volume to store dashboard configuration. With one docker compose up -d, you have a metric data warehouse and a place to view it.
Once the stack is alive, run the test while writing to InfluxDB:
k6 run -o influxdb=http://localhost:8086/k6 script.jsGrafana is the panel where the performance story is read. The setup steps:
http://influxdb:8086 (the Compose service name; internal DNS works automatically) and the database with k6.SELECT mean("value") FROM "http_req_duration" WHERE $timeFilter GROUP BY time($__interval) fill(null)http_req_duration), error rate (http_req_failed), request count (http_reqs), and active VUs (vus). These four are the four corners of the same question: "is the system still healthy, and at what point does it start to suffer?"From here emerges the analysis that's impossible through a terminal: a latency graph rising together with VU growth shows the capacity point; an error line hitting the ceiling at a certain minute points to an event that needs investigation. Dashboards can also be shared — the team can see weekly trends, not just one night's results.
Without tags, all requests are mixed into a single http_req_duration number. Yet login, product listing, and checkout have very different latency profiles. Tags are labels attached to data points, and they're what turns a dashboard from "what's the whole app's average" into "which endpoint is suffering".
Test-wide tags are defined in the options and apply to all metrics:
export const options = {
vus: 50,
duration: "5m",
tags: {
environment: "staging",
release: __ENV.RELEASE_TAG || "dev",
},
};Per-request tags separate data within a single test — this is what you use to compare endpoints:
const res = http.get("https://api.example.com/v1/login", {
tags: { endpoint: "login" },
});
const res2 = http.get("https://api.example.com/v1/checkout", {
tags: { endpoint: "checkout" },
});Tag values you want to control from outside can be injected via -e and read through __ENV — the same pattern as secrets in episode 12, but for non-confidential data:
k6 run -e K6_TAGS=staging -e RELEASE_TAG=v1.2.3 script.jsIn Grafana, tags become columns that can be used as filters: WHERE "endpoint"='login' separates the login curve from checkout; environment='staging' compares results between environments. Without tagging, this kind of analysis is impossible — and with tagging, your dashboard answers much sharper questions.
k6 measures request latency, but there are things it doesn't measure automatically — for example how long JSON parsing takes on the client side, or response size. For that, k6 provides custom metrics. Trend is the most useful type for values that change over time, and it gets full treatment: statistics, percentiles, and it can be displayed on a dashboard:
import { Trend } from "k6/metrics";
import http from "k6/http";
import { check } from "k6";
const jsonParseTime = new Trend("json_parse_time", true);
export default function () {
const res = http.get("https://api.example.com/v1/data");
check(res, { "response 200": (r) => r.status === 200 });
const start = Date.now();
JSON.parse(res.body);
jsonParseTime.add(Date.now() - start);
}The second argument true in new Trend("json_parse_time", true) indicates that the values fed in will be treated as durations (rendered with time units in the summary and dashboard). Now json_parse_time appears in the summary and in InfluxDB like a built-in metric — complete with its own p(95). This is how you add performance dimensions nobody thought of before, and it's the essence of observability: not just looking at the data that exists, but collecting the data that answers your team's specific questions.
environment, release, and endpoint from day one — it's easier to add tags than to rewind time.true (duration) or false (regular value) according to their semantics.In this episode 14 you've brought k6 results out of the terminal: understanding the three main outputs (json, influxdb, and experimental-prometheus-rw), assembling the local InfluxDB + Grafana + k6 stack with Docker Compose, building a Grafana dashboard with queries and panels that answer performance questions, enriching data with test-wide and per-request tags (including injecting tag values via -e), and creating Trend custom metrics to measure things k6 doesn't.
Key points to take away:
-o can be stacked: a JSON file for archives, a database for dashboards.With observability, you've traveled one full k6 learning cycle from zero: from writing your first script to seeing application performance in curves and trends that can be shared with the team. But the real world doesn't always produce smooth curves. In the next episode, 15, we'll cover Resilience & Failure Analysis — how to load scenarios that contain failures (retry, timeouts, backpressure) so you know your application still stands even when things aren't going well. See you in episode 15!