Learn k6 - Observability, Tracing & Monitoring
Series/Learn k6/Episode 14
Episode 14 of 19

Learn k6 - Observability, Tracing & Monitoring

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.

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

Introduction

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.

Main Discussion

External Output: Bringing Data Out of the Terminal

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:

Output to a JSON file for post-processing
k6 run -o json=results.json script.js
Output to InfluxDB
k6 run -o influxdb=http://localhost:8086/k6 script.js
Output to Prometheus via remote write
k6 run -o experimental-prometheus-rw=http://localhost:9090/api/v1/write script.js
  • json=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.

Local Stack: InfluxDB + Grafana + k6

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:

compose.yaml — InfluxDB + Grafana + k6
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:

Run the test while writing to InfluxDB
k6 run -o influxdb=http://localhost:8086/k6 script.js

Grafana Dashboard: Turning Data into Decisions

Grafana is the panel where the performance story is read. The setup steps:

  1. Add a data source. In Grafana, Data Sources → Add data source → select InfluxDB. Fill the URL with http://influxdb:8086 (the Compose service name; internal DNS works automatically) and the database with k6.
  2. Build a query. k6 data is stored as measurements named after the metrics. A simple query for latency:
InfluxDB query — request latency over time
SELECT mean("value") FROM "http_req_duration" WHERE $timeFilter GROUP BY time($__interval) fill(null)
  1. Arrange the panels. A good k6 dashboard consists of at least four panels: latency (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.

Tagging: Breaking Down Results per Endpoint and Scenario

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:

options — tags for the whole test
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:

Per-request tags in params
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:

Inject tag values from outside
k6 run -e K6_TAGS=staging -e RELEASE_TAG=v1.2.3 script.js

In 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.

Custom Metrics: Measuring What k6 Doesn't

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:

custom-metrics.js — a custom Trend metric
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.

Observability Best Practices

  • Save important results to a JSON file as an archive, alongside the database for dashboards.
  • Tag environment, release, and endpoint from day one — it's easier to add tags than to rewind time.
  • Build the dashboard with latency, error, throughput, and VU panels as the foundation.
  • Mark custom metrics with true (duration) or false (regular value) according to their semantics.
  • Verify once that the data actually reaches InfluxDB/Prometheus before running long tests.

Conclusion

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.
  • InfluxDB + Grafana is the fastest combination for local k6 observability.
  • Tags are the key to per-endpoint analysis; start tagging early.
  • Custom metrics let you measure dimensions relevant to your application's context.

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!

Learn k6 - Observability, Tracing & Monitoring | Learn k6