On this page8 sections
CommandsIntermediate6-8 min reference

Grafana & InfluxDB for QA

A practical guide to wiring up Grafana + InfluxDB for the metrics testers actually care about — k6 load runs, CI/CD trends, flaky-test tracking, and alerting.

InfluxDB Basics

InfluxDB is a time-series database. Every row has a timestamp; columns split into tags (indexed metadata) and fields (the actual numeric values you graph).

Concepts

ConceptWhat it isExample
Database (1.x) / Bucket (2.x)Top-level container for time-series datak6, ci_metrics
MeasurementEquivalent to a table; groups related pointshttp_req_duration, test_run
TagIndexed metadata (string only). Fast to filter / group bymethod=GET, status=200, env=staging
FieldThe numeric value(s) you graph. Not indexedvalue=247.3, count=42
TimestampWhen the point happened (auto if omitted)2026-05-03T10:00:00Z
SeriesUnique combo of measurement + tag sethttp_req_duration,method=GET,status=200
Retention policyHow long data is kept before being dropped7d, 30d, inf

Tags vs Fields — the rule

Tag it if you'll filter or group by it. Field it if you'll do math on it.

Tags are indexed; fields aren't. Putting a high-cardinality value (UUIDs, full URLs) in a tag explodes the index — that's the most common InfluxDB performance bug.

UseTagField
WHERE method='GET'✗ slow
GROUP BY status✗ slow
MEAN(value)✗ tags are strings
> 500 threshold

Writing data with curl — Line Protocol

Line Protocol is the wire format: measurement,tag1=v,tag2=v field1=v,field2=v timestamp.

# InfluxDB 1.x
curl -i -XPOST 'http://localhost:8086/write?db=k6' --data-binary \
  'http_req_duration,method=GET,status=200,env=staging value=247 1714723200000000000'
 
# InfluxDB 2.x — bucket + org + token
curl -i -XPOST "http://localhost:8086/api/v2/write?org=qa&bucket=k6&precision=ns" \
  -H "Authorization: Token $INFLUX_TOKEN" \
  --data-raw \
  'http_req_duration,method=GET,status=200,env=staging value=247'

Querying — InfluxQL (1.x and 2.x compatibility)

-- Mean response time, 1-minute buckets, last hour
SELECT mean("value")
FROM "http_req_duration"
WHERE time > now() - 1h
GROUP BY time(1m), "method"
fill(null);
 
-- p95 latency by endpoint
SELECT percentile("value", 95) AS p95
FROM "http_req_duration"
WHERE time > now() - 6h
GROUP BY "name";
 
-- Error rate (errors / total)
SELECT count("value") AS errors
FROM "http_req_failed"
WHERE "error" = '1' AND time > now() - 1h
GROUP BY time(1m);
 
-- Top 5 slowest endpoints
SELECT max("value") AS max_ms
FROM "http_req_duration"
WHERE time > now() - 1h
GROUP BY "name"
ORDER BY max_ms DESC
LIMIT 5;

Querying — Flux (InfluxDB 2.x native)

from(bucket: "k6")
  |> range(start: -1h)
  |> filter(fn: (r) => r._measurement == "http_req_duration")
  |> filter(fn: (r) => r.method == "GET")
  |> aggregateWindow(every: 1m, fn: mean, createEmpty: false)
  |> yield(name: "mean")
 
// p95 by endpoint
from(bucket: "k6")
  |> range(start: -6h)
  |> filter(fn: (r) => r._measurement == "http_req_duration")
  |> group(columns: ["name"])
  |> quantile(q: 0.95, method: "estimate_tdigest")

Flux is more powerful (joins, pivots, alerts in-line) but verbose. Most QA dashboards stick to InfluxQL — it's enough for 95% of cases.

Retention policies

-- 1.x — 30-day retention on database 'k6'
CREATE RETENTION POLICY "thirty_days" ON "k6" DURATION 30d REPLICATION 1 DEFAULT;
 
-- Drop after 7 days for a busy bucket
CREATE RETENTION POLICY "one_week" ON "ci_metrics" DURATION 7d REPLICATION 1;

In 2.x, retention is set per-bucket at create time:

influx bucket create --name k6 --retention 30d --org qa

Local InfluxDB via Docker

# 2.x — easiest for new setups
docker run -d --name influxdb -p 8086:8086 influxdb:2.7
 
# Open http://localhost:8086 — first-run setup wizard creates org, bucket, token
 
# 1.x — still common in older k6 examples
docker run -d --name influxdb -p 8086:8086 -e INFLUXDB_DB=k6 influxdb:1.8

K6 → InfluxDB Integration

k6 ships with an InfluxDB output. Pipe results into a bucket; Grafana reads the same bucket for dashboards.

Run with the InfluxDB output

# InfluxDB 1.x
k6 run --out influxdb=http://localhost:8086/k6 script.js
 
# InfluxDB 2.x (xk6 extension — built into recent k6 versions)
k6 run --out influxdb=http://localhost:8086 \
  -e K6_INFLUXDB_ORGANIZATION=qa \
  -e K6_INFLUXDB_BUCKET=k6 \
  -e K6_INFLUXDB_TOKEN=$INFLUX_TOKEN \
  script.js

What ends up in InfluxDB

k6 writes one measurement per built-in metric:

MeasurementWhat it holds
http_req_durationRequest total duration (ms)
http_req_blockedTime spent waiting for a free TCP connection
http_req_connectingTCP connect time
http_req_tls_handshakingTLS handshake time
http_req_sendingTime sending the request
http_req_waitingServer processing time (TTFB)
http_req_receivingTime downloading the response
http_req_failedRate metric — 1 for failed, 0 for OK
http_reqsCounter — total requests
vusCurrently active virtual users
vus_maxMax VUs reached
iterationsCounter — total iterations completed
iteration_durationTime per iteration (ms)
data_sent, data_receivedBytes
checksPass/fail rate of check() calls

Custom k6 metrics also write to InfluxDB:

import { Counter, Trend } from 'k6/metrics';
 
export const errors = new Counter('app_errors');
export const dbTime = new Trend('app_db_time');

→ measurements app_errors and app_db_time appear automatically.

Tags k6 attaches by default

Every point carries these tags — use them to filter and group:

TagExampleUseful for
methodGET, POSTPer-method dashboards
urlhttps://api.example.com/users/42Specific endpoint analysis
namehttps://api.example.com/users/{id}Templated URL grouping (set via tags: { name: ... })
status200, 404, 500Error breakdown
grouplogin flowk6 group() blocks
scenariopeak_loadWhen using k6 scenarios
expected_responsetrue, falseWhether the response counted as expected

Always set name explicitly on requests with dynamic IDs — /users/42, /users/43 would otherwise create a series per user.

http.get(`/users/${id}`, { tags: { name: '/users/{id}' } });

Grafana Setup

Standalone Grafana

docker run -d --name grafana -p 3000:3000 grafana/grafana
 
# Visit http://localhost:3000  — default login: admin / admin

Docker Compose — InfluxDB + Grafana stack

The setup most teams actually use. Saves credentials, dashboards, and dashboards survive restarts.

services:
  influxdb:
    image: influxdb:2.7
    ports:
      - "8086:8086"
    environment:
      DOCKER_INFLUXDB_INIT_MODE: setup
      DOCKER_INFLUXDB_INIT_USERNAME: admin
      DOCKER_INFLUXDB_INIT_PASSWORD: admin12345
      DOCKER_INFLUXDB_INIT_ORG: qa
      DOCKER_INFLUXDB_INIT_BUCKET: k6
      DOCKER_INFLUXDB_INIT_RETENTION: 30d
      DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: dev-token-please-rotate
    volumes:
      - influxdb-data:/var/lib/influxdb2
      - influxdb-config:/etc/influxdb2
 
  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    environment:
      GF_SECURITY_ADMIN_PASSWORD: admin
      GF_AUTH_ANONYMOUS_ENABLED: "true"          # public dashboards
      GF_AUTH_ANONYMOUS_ORG_ROLE: Viewer
      GF_INSTALL_PLUGINS: "grafana-clock-panel"
    volumes:
      - grafana-data:/var/lib/grafana
      - ./provisioning:/etc/grafana/provisioning  # see below
      - ./dashboards:/var/lib/grafana/dashboards
    depends_on:
      - influxdb
 
volumes:
  influxdb-data:
  influxdb-config:
  grafana-data:
docker compose up -d

Add InfluxDB as a data source

In the Grafana UI: Connections → Data sources → Add → InfluxDB.

FieldValue
Query languageFlux (2.x) or InfluxQL (1.x or compat)
URLhttp://influxdb:8086 (Compose service name) or http://localhost:8086
Organizationqa
Default Bucketk6
Tokenthe admin token from the env vars

Click Save & Test — should report data source is working.

For automated provisioning, drop a YAML in provisioning/datasources/:

apiVersion: 1
datasources:
  - name: InfluxDB-k6
    type: influxdb
    access: proxy
    url: http://influxdb:8086
    isDefault: true
    jsonData:
      version: Flux
      organization: qa
      defaultBucket: k6
    secureJsonData:
      token: ${INFLUX_TOKEN}

K6 Performance Dashboard

The panels every k6 dashboard should have. Build them in this order — each answers a different question.

1. Request rate (req/s)

SELECT count("value") / 5 AS rps
FROM "http_reqs"
WHERE $timeFilter
GROUP BY time(5s) fill(0)

Visualisation: Time series. Shows traffic shape — ramp-up, steady state, ramp-down.

2. Response time percentiles — p50 / p90 / p95 / p99

SELECT percentile("value", 50) AS "p50",
       percentile("value", 90) AS "p90",
       percentile("value", 95) AS "p95",
       percentile("value", 99) AS "p99"
FROM "http_req_duration"
WHERE $timeFilter
GROUP BY time(10s) fill(null)

Visualisation: Time series, multiple lines. The most important latency view — averages hide the tail.

3. Error rate gauge

SELECT mean("value") * 100 AS "error_rate_pct"
FROM "http_req_failed"
WHERE $timeFilter

Visualisation: Gauge with thresholds: 0–1% green, 1–5% yellow, > 5% red.

4. Active VUs

SELECT max("value")
FROM "vus"
WHERE $timeFilter
GROUP BY time(5s) fill(null)

Visualisation: Time series, often overlaid with the request-rate panel to correlate user load with latency.

5. Response time by endpoint

SELECT percentile("value", 95)
FROM "http_req_duration"
WHERE $timeFilter
GROUP BY time(30s), "name" fill(null)

Visualisation: Time series with one line per name tag. Surfaces which endpoint regresses first under load.

6. Throughput by method

SELECT count("value") / 60 AS "req/s"
FROM "http_reqs"
WHERE $timeFilter
GROUP BY time(1m), "method" fill(0)

Visualisation: Time series, stacked area. Quick view of read vs write traffic mix.

7. Status code distribution

SELECT count("value")
FROM "http_reqs"
WHERE $timeFilter
GROUP BY time(30s), "status" fill(0)

Visualisation: Stat for current values + time series for trend. Color-code: 2xx green, 3xx blue, 4xx yellow, 5xx red.

Dashboard variables

Variables let one dashboard cover multiple environments / scenarios.

Name: env
Type: Query
Query: SHOW TAG VALUES FROM "http_reqs" WITH KEY = "env"

Name: endpoint
Type: Query
Query: SHOW TAG VALUES FROM "http_req_duration" WITH KEY = "name"
Multi-value: ✓
Include All: ✓

Then in queries:

WHERE $timeFilter AND "env" =~ /^$env$/ AND "name" =~ /^$endpoint$/

The dashboard now has dropdowns for env and endpoint at the top.

Time range selector

Grafana's built-in $__timeFilter() (Flux) and $timeFilter (InfluxQL) bind to the time picker in the top-right. Set sensible defaults at the dashboard level: now-1h to now for live ops, now-7d to now for trend dashboards.

Test Result Dashboards

Beyond k6 — InfluxDB + Grafana also work for CI/CD test result trends. Push a single point per CI run.

Schema

Field / tagGoes inNotes
passed (count)fieldNumber of passing tests
failed (count)fieldNumber of failing tests
duration_msfieldTotal run time
coverage_pctfieldCode coverage
flaky (count)fieldTests that retried
branchtagmain, pr/123
suitetage2e, unit, smoke
committag (LOW cardinality only — careful)Often better as a field

Posting from CI

PASSED=42
FAILED=3
DURATION=187432
 
curl -i -XPOST "http://influxdb:8086/api/v2/write?org=qa&bucket=ci_metrics&precision=ms" \
  -H "Authorization: Token $INFLUX_TOKEN" \
  --data-raw \
  "test_run,branch=main,suite=e2e passed=${PASSED}i,failed=${FAILED}i,duration_ms=${DURATION}i"

Add to your GitHub Actions workflow as a step right after the test run.

Useful panels

PanelQueryWhat it shows
Pass/fail rate over timeSELECT sum(passed)/(sum(passed)+sum(failed))*100 FROM "test_run" GROUP BY time(1d)Trend of suite health
Run duration trendSELECT mean(duration_ms)/1000 FROM "test_run" GROUP BY time(1d)Catches slow-creep regressions
Failures by suiteSELECT sum(failed) FROM "test_run" GROUP BY time(1d), "suite"Stacked area — which suite is unhealthy?
Coverage trendSELECT last(coverage_pct) FROM "test_run" GROUP BY time(1d)Coverage direction
Flaky-test countSELECT sum(flaky) FROM "test_run" GROUP BY time(1d)Track flake-fix campaigns
Top flaky testsPush per-test flake counts as flaky_test,test=login_test count=3Identify which tests need fixing

Annotations for releases

Push a deployment marker so dashboards show "we deployed at this point":

curl -XPOST "http://grafana:3000/api/annotations" \
  -H "Authorization: Bearer $GRAFANA_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"text\": \"Deploy v1.2.3 to staging\",
    \"tags\": [\"deploy\", \"staging\"],
    \"time\": $(date +%s)000
  }"

A vertical line + label appears on every panel — great for spotting regressions caused by a specific deploy.

Alerting

Grafana 9+ ships unified alerting that works for any data source.

Defining a rule

  1. Create alert rule on a panel → "More → New alert rule".
  2. Pick the query — typically a function over the same data the panel shows.
  3. Set the condition — threshold or math expression.
Query A:  p95 latency for /checkout (last 5 min)
Reduce:   last()
Condition: A > 2000

For: 5 minutes      ← only fire if condition holds for 5 min

Common alert conditions for QA

AlertConditionSeverity
Test suite failedfailed > 0 from test_run measurementhigh
Pass rate droppedpass_rate < 95 for last 6 runshigh
p95 latency > 2 spercentile("value", 95) > 2000 from http_req_durationwarning
Error rate > 5 %mean("http_req_failed") > 0.05 for 5 minwarning
Throughput droppedcount("http_reqs") < expected_baseline * 0.5warning
Flaky-test count risingday-over-day delta > 10 in flaky fieldlow
Synthetic check failedcheck_status field == 0 for 2 consecutive runshigh

Notification channels

Grafana calls them contact points. Common ones:

ChannelSetup notes
SlackIncoming webhook URL → channel; supports message templates
EmailSMTP configured at server level
PagerDutyIntegration key from a PagerDuty service
Microsoft TeamsIncoming webhook URL
WebhookPosts JSON to any URL — useful for custom routing or to your own bot
OpsgenieAPI key

Notification policies

Group alerts by labels (severity=high → PagerDuty, severity=warning → Slack, severity=low → email-digest) so you don't page someone for a flaky-test counter ticking up.

Silences

Maintenance window or known issue? Create a silence — matches by label, valid for a time window. Alerts in the silenced range still evaluate but don't notify.

Silence: env="staging" AND alertname="p95 latency"
From:    2026-05-03 08:00 UTC
To:      2026-05-03 12:00 UTC
Comment: Staging upgrade — expect latency spikes

Prometheus Alternative

Prometheus is the other common back end. Many teams use it instead of (or alongside) InfluxDB for metrics.

k6 → Prometheus integration

# Remote-write to Prometheus
K6_PROMETHEUS_RW_SERVER_URL=http://prometheus:9090/api/v1/write \
  k6 run --out experimental-prometheus-rw script.js
 
# Or scrape mode — k6 exposes /metrics, Prometheus pulls
k6 run --out experimental-prometheus-rw script.js

PromQL vs InfluxQL

# PromQL — p95 latency over 5 min
histogram_quantile(0.95, rate(http_req_duration_bucket[5m]))
 
# Error rate
sum(rate(http_req_failed_total[5m])) / sum(rate(http_reqs_total[5m]))
 
# Top 5 endpoints by request rate
topk(5, sum by (name) (rate(http_reqs_total[5m])))
-- Equivalent InfluxQL
SELECT percentile("value", 95) FROM "http_req_duration" WHERE time > now() - 5m
SELECT mean("value") FROM "http_req_failed" WHERE time > now() - 5m
SELECT count("value") FROM "http_reqs" WHERE time > now() - 5m GROUP BY "name" ORDER BY count DESC LIMIT 5

When to use which

Choose Prometheus when…Choose InfluxDB when…
The rest of your stack already uses itYou need long-term retention (months / years)
You want pull-based scrapingYou want push-based writes from CI
Your team already knows PromQLYou need flexible event/metadata querying
You're running Kubernetes (prom-operator is standard)You're storing test runs (low frequency, high cardinality on tags like commit)
Short-to-medium retention is fine (days–weeks)You want a single TSDB for both real-time and historical

For pure load-test result storage, InfluxDB is usually a better fit — Prometheus is optimised for short-lived high-frequency metrics, not for keeping every k6 run for a year.

Useful Grafana Features for QA

Annotations for deployments and test runs

Vertical markers on every panel — answers "did the regression start at the deploy, or before?"

SourceHow
ManualClick on a panel timeline → Add annotation
API from CIPOST /api/annotations with bearer token
Auto from Git tagsGrafana annotation query against your CI database (SELECT … FROM deploys)

Dashboard variables

Build one dashboard, reuse across environments / scenarios / branches. Variable types:

  • Query — populated from a data source query (SHOW TAG VALUES FROM "x" WITH KEY = "env")
  • Custom — static dropdown
  • Constant — hidden, used for templating
  • Datasource — switch the dashboard between dev / staging / prod data sources
  • Interval — pick the GROUP BY time bucket (1m, 5m, 1h)

Snapshots

Share a dashboard at a moment in time, with the data baked in — no need to grant data-source access. Useful for incident postmortems and PR comments.

Dashboard menu → Share → Snapshot → Local snapshot

Playlists

Auto-cycle through dashboards on an office screen — Smoke health → Performance → Coverage → CI flakiness on a 30-second loop. Set up under Dashboards → Playlists.

Embedding panels

<iframe> a single panel into wikis, runbooks, or the team intranet:

Panel menu → Share → Embed

Set the time range and theme (light/dark) in the URL params:

https://grafana.example.com/d-solo/abc/load-tests?panelId=4&from=now-7d&to=now&theme=dark

Provisioning dashboards as JSON in Git

The most important practice for any team beyond a single dashboard: dashboards should be version-controlled.

provisioning/
├── dashboards/
│   └── dashboards.yml          ← tells Grafana where to look
└── datasources/
    └── influxdb.yml

dashboards/
├── k6-performance.json
├── ci-test-trends.json
└── flaky-tests.json

provisioning/dashboards/dashboards.yml:

apiVersion: 1
providers:
  - name: 'qa-dashboards'
    folder: 'QA'
    type: file
    options:
      path: /var/lib/grafana/dashboards

In docker-compose.yml, mount both directories (shown in the Compose example above). Now the dashboards live in Git, deploy with the rest of your infra, and survive a fresh Grafana container.

Importing community dashboards

Grafana hosts a public dashboard library. For k6, use:

  • Dashboard ID 2587 — k6 Load Testing Results (InfluxDB 1.x)
  • Dashboard ID 14801 — k6 + Prometheus
Grafana UI → Dashboards → Import → paste ID → pick data source

Use these as a starting template; clone, edit panels for your tag schema, then commit the JSON to your repo.