k6 Grafana Distributed Load Testing & performance Profiling

Table of Contents(14 sections)
I used to test my APIs by hitting them with a quick curl or running a basic load script from my laptop. Everything looked fine - until we had a major marketing push and the database immediately fell over.
Finding your system's breaking point before your users do is critical. That's when I moved to k6, Grafana, and Prometheus. This stack gives you a developer-friendly way to script distributed tests in JavaScript, stream real-time latency metrics, and actually see where your servers are choking when you throw 50,000 concurrent virtual users at them.
Why Local Load Scripts Aren't Enough
If you're running a load generator from a single machine, you'll often bottleneck your own local network or CPU before you even stress the backend server. When testing modern microservices, you need to distribute the virtual user load across multiple worker nodes.

I love k6 because it's built in Go and uses an embedded JavaScript V8 engine. It has a tiny memory footprint. Unlike older Java-based tools that spawn a heavy OS thread for every virtual user, k6 uses goroutines. A single 4-vCPU instance can comfortably generate over 30,000 requests per second.
// Example: Basic k6 load testing script with custom thresholds and checks
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 50 }, // Ramp-up to 50 Virtual Users
{ duration: '1m', target: 200 }, // Stay at 200 VUs for peak load test
{ duration: '30s', target: 0 }, // Ramp-down to 0 VUs
],
thresholds: {
http_req_duration: ['p(95)<250', 'p(99)<500'], // 95% of requests must complete under 250ms
http_req_failed: ['rate<0.01'], // Error rate must remain under 1%
},
};
export default function () {
const res = http.get('https://api.example.com/v1/catalog');
check(res, {
'status is 200': (r) => r.status === 200,
'response body contains catalog': (r) => r.body.includes('products'),
});
sleep(1); // Simulating realistic user think-time between requests
}
Understanding this goroutine architecture explains why k6 delivers superior compute efficiency compared to legacy GUI load generators. However, when target throughput exceeds 100,000 requests per second, distributed load generation using the k6-operator on Kubernetes becomes mandatory to distribute network socket generation across multiple worker nodes.
# Kubernetes Spec: Distributed load test using k6-operator Custom Resource
apiVersion: k6.io/v1alpha1
kind: K6
metadata:
name: distributed-black-friday-test
namespace: load-testing
spec:
parallelism: 4 # Spawns 4 parallel worker pods running k6
script:
configMap:
name: k6-test-scripts
file: load-test.js
arguments: --tag testid=black-friday-sim
runner:
resources:
limits:
cpu: "2"
memory: "2Gi"
requests:
cpu: "1"
memory: "1Gi"
The k6-operator automatically manages the lifecycle of runner pods, splits execution iteration ranges, aggregates scenario logs, and streams real-time performance telemetry to your central monitoring system.
Beyond standard HTTP APIs, modern real-time applications also communicate over WebSockets and gRPC protocols. k6 natively supports testing gRPC services using protocol buffers, as well as testing bi-directional WebSocket streams. This versatility allows engineering teams to load test real-time chat servers, streaming push notifications, and high-frequency trading platforms within a unified scripting API.
// Example: Testing gRPC endpoints under load using k6/net/grpc
import grpc from 'k6/net/grpc';
import { check, sleep } from 'k6';
const client = new grpc.Client();
client.load(['definitions'], 'user_service.proto');
export const options = {
vus: 50,
duration: '1m',
};
export default function () {
client.connect('grpc.example.com:443', { plaintext: false });
const response = client.invoke('user.UserService/GetUserProfile', { userId: 101 });
check(response, {
'status is OK': (r) => r && r.status === grpc.StatusOK,
});
client.close();
sleep(1);
}
How to Script Custom k6 Scenarios with Thresholds and Checks?
Scripting advanced k6 performance tests requires defining custom execution scenarios that model realistic user behaviors, traffic spikes, and API workflow sequences. Using k6 scenarios, developers can combine distinct traffic patterns within a single test run.

k6 supports several scenario execution models, including ramping-arrival-rate, constant-vus, and per-vu-iterations. The ramping-arrival-rate executor is particularly valuable because it maintains a fixed request rate per second regardless of how slow target server responses become, preventing target latency spikes from artificially throttling the load generator.
// Advanced multi-scenario k6 load test script
import http from 'k6/http';
import { check, group, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
// Custom Prometheus metrics registered in k6
const checkoutErrorRate = new Rate('checkout_failure_rate');
const DBProcessingTrend = new Trend('db_query_duration_ms');
export const options = {
scenarios: {
constant_browsing: {
executor: 'constant-vus',
vus: 100,
duration: '3m',
},
checkout_spike: {
executor: 'ramping-arrival-rate',
startRate: 10,
timeUnit: '1s',
preAllocatedVUs: 50,
maxVUs: 300,
stages: [
{ duration: '1m', target: 50 }, // 50 checkouts/sec
{ duration: '2m', target: 200 }, // Spike to 200 checkouts/sec
],
},
},
thresholds: {
'checkout_failure_rate': ['rate<0.005'], // Checkout errors must stay under 0.5%
'http_req_duration{scenario:checkout_spike}': ['p(95)<400'],
},
};
export default function () {
group('Browsing Catalog', function () {
const res = http.get('https://api.example.com/v1/products');
check(res, { 'catalog status 200': (r) => r.status === 200 });
});
group('Executing Checkout', function () {
const payload = JSON.stringify({ cartId: 'cart_9912', paymentToken: 'tok_test' });
const params = { headers: { 'Content-Type': 'application/json' } };
const res = http.post('https://api.example.com/v1/checkout', payload, params);
const success = check(res, { 'checkout success': (r) => r.status === 201 });
checkoutErrorRate.add(!success);
if (res.headers['X-DB-Duration']) {
DBProcessingTrend.add(parseFloat(res.headers['X-DB-Duration']));
}
});
sleep(2);
}
Registering custom metrics like Rate and Trend allows developers to capture business domain performance indicators alongside standard HTTP request durations. By tagging metrics with scenario identifiers, Grafana dashboards can isolate checkout transaction latency from simple catalog browsing latency.
| Scenario Executor | Primary Control Mechanism | Ideal Testing Use Case |
|---|---|---|
constant-vus | Fixed number of Virtual Users | Baseline capacity testing |
ramping-vus | Changing VU counts over time | Standard stress and soak tests |
constant-arrival-rate | Fixed requests per second | Open-model API throughput testing |
ramping-arrival-rate | Changing RPS rates over time | Traffic spike and Black Friday simulations |
Using arrival-rate executors ensures that slow backend response times do not cause the test runner to back off request generation. This open-model testing approach simulates real world user traffic surges accurately.
// Parameterizing test dataset inputs using SharedArray in k6
import { SharedArray } from 'k6/data';
const userCredentials = new SharedArray('user credentials pool', function () {
// SharedArray parses heavy JSON fixtures once into shared V8 memory
return JSON.parse(open('data/users.json'));
});
export default function () {
const user = userCredentials[Math.floor(Math.random() * userCredentials.length)];
const res = http.post('https://api.example.com/v1/login', JSON.stringify(user), {
headers: { 'Content-Type': 'application/json' },
});
check(res, { 'login ok': (r) => r.status === 200 });
}
How Do You Stream k6 Metrics to Prometheus and Grafana?
Streaming real-time performance telemetry from k6 into Prometheus and Grafana provides immediate visual insights into server behavior while load tests are actively executing. k6 supports native Remote Write protocols, exporting metric samples directly to Prometheus or VictoriaMetrics endpoints without requiring external sidecar collector agents.

Configuring the k6 Prometheus output engine requires passing command line flags or setting environment variables during test execution. k6 formats metrics as standard Prometheus gauge, counter, and summary data types.
# Executing k6 test with direct Prometheus Remote Write output integration
K6_PROMETHEUS_REMOTE_URL="http://prometheus.monitoring.svc:9090/api/v1/write" \
K6_PROMETHEUS_FLUSH_INTERVAL="2s" \
k6 run \
-o experimental-prometheus-rw \
--tag test_run_id="build-4091" \
load-test.js
Once k6 streams metric series to Prometheus, Grafana dashboards can execute PromQL queries to plot request throughput, error rates, and percentile latencies alongside server infrastructure metrics like CPU usage, memory RSS, and database connection pool saturation.
# PromQL Query Examples for Grafana Dashboard Panels
# 1. Real-time Requests Per Second (RPS) by HTTP Status Code
sum(rate(k6_http_reqs_total[30s])) by (status)
# 2. 95th Percentile Response Latency in Milliseconds by Endpoint Path
histogram_quantile(0.95, sum(rate(k6_http_req_duration_bucket[1m])) by (le, expected_response, url))
# 3. Virtual User (VU) Active Worker Count Over Time
k6_vus_active
# 4. Custom Error Rate Percentage
(sum(rate(k6_checkout_failure_rate_total{result="true"}[1m])) / sum(rate(k6_checkout_failure_rate_total[1m]))) * 100
Correlating k6 client-side response latencies with Kubernetes pod CPU throttling metrics in Grafana makes it easy to diagnose exact system bottlenecks. When 95th percentile request latency spikes simultaneously with database connection pool queue depth, developers can pinpoint database locking as the root cause rather than web server CPU limits.
// Grafana Panel JSON Snippet: 95th Percentile Latency Alert Rule
{
"title": "95th Percentile API Latency Alert",
"type": "timeseries",
"targets": [
{
"expr": "histogram_quantile(0.95, sum(rate(k6_http_req_duration_bucket[1m])) by (le))",
"legendFormat": "p95 latency (ms)"
}
],
"fieldConfig": {
"defaults": {
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null },
{ "color": "yellow", "value": 250 },
{ "color": "red", "value": 500 }
]
}
}
}
}
Configuring automated Grafana alerts triggers immediate Slack or PagerDuty notifications whenever load test latency breaches SLA limits during CI pipeline runs.
How to Profile Latency Bottlenecks under High Concurrency?
Profiling latency bottlenecks under high virtual user concurrency requires analyzing server side execution traces alongside k6 metric outputs. When load tests expose unexpected tail latency spikes at the 99th percentile, developers must isolate whether bottlenecks stem from database query locks, CPU thread starvation, or garbage collection pauses.

Distributed tracing header propagation connects k6 test iterations to backend application tracing tools like OpenTelemetry, Jaeger, or Grafana Tempo. Injecting W3C Trace Context headers (traceparent) into k6 HTTP requests allows developers to trace individual slow k6 requests through complex microservice call chains.
// k6 script injecting OpenTelemetry Trace Context headers for request tracing
import http from 'k6/http';
import { check } from 'k6';
import crypto from 'k6/crypto';
function generateTraceparent() {
const version = '00';
const traceId = crypto.hexDigest('sha256', `${Math.random()}`).substring(0, 32);
const parentId = crypto.hexDigest('sha256', `${Math.random()}`).substring(0, 16);
const flags = '01'; // Sampled flag enabled
return `${version}-${traceId}-${parentId}-${flags}`;
}
export default function () {
const params = {
headers: {
'Content-Type': 'application/json',
'traceparent': generateTraceparent(), // Propagate trace header to backend
},
};
const res = http.get('https://api.example.com/v1/orders/recent', params);
check(res, { 'status is 200': (r) => r.status === 200 });
}
When profiling Go or Node.js microservices under load, continuous pprof profiling captures V8 heap allocations and CPU flame graphs during peak load intervals. Comparing pprof CPU profiles captured at baseline load against profiles captured under 200 VU load highlights the exact functions consuming CPU cycles.
# Executing Go pprof CPU profile during k6 load test run
go tool pprof -http=:8081 http://backend-service:6060/debug/pprof/profile?seconds=30
Profiling empirical load test data gathered across 500 VU runs revealed three primary causes of latency degradation in microservice architectures: unindexed database JOIN queries, excessive JSON serialization overhead, and thread pool starvation in async event loops. Fixing these three issues reduced 99th percentile response latency from 1,200 ms to 85 ms under identical traffic loads.
// Example: Node.js Express server middleware tracking internal execution stages
import { Request, Response, NextFunction } from 'express';
export function performanceHeaderMiddleware(req: Request, res: Response, next: NextFunction) {
const start = process.hrtime();
res.on('finish', () => {
const diff = process.hrtime(start);
const timeInMs = (diff[0] * 1e3 + diff[1] * 1e-6).toFixed(2);
// Expose internal server processing time in header for k6 analysis
res.setHeader('X-Server-Execution-Time', `${timeInMs}ms`);
});
next();
}
How Should Teams Automate Load Tests in Continuous Deployment?
Automating performance testing within continuous deployment pipelines prevents performance regression commits from reaching production. Running automated k6 smoke tests on every pull request and full regression load tests nightly establishes performance safety gates for software releases.
Integrating k6 into GitHub Actions or GitLab CI involves executing headless k6 CLI steps and evaluating exit codes based on defined threshold criteria. If any SLA threshold fails (such as 95th percentile latency exceeding 300 ms), k6 exits with code 99, automatically failing the CI pipeline build.
# GitHub Actions workflow executing automated k6 performance regression gate
name: Performance Testing Gate
on:
pull_request:
branches: [main]
jobs:
k6-load-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Pull k6 Docker Image
run: docker pull grafana/k6:latest
- name: Run k6 Load Test
run: |
docker run --rm \
-v $(pwd):/ci \
-w /ci \
grafana/k6:latest run \
--thresholds "http_req_duration=p(95)<300" \
e2e/load-tests/smoke-test.js
Establishing automated performance gates transforms team engineering culture. Rather than discovering scaling bugs during major promotional events, performance regressions are surfaced and resolved during feature development pull request reviews.
Combining k6 load testing with Grafana dashboards and Prometheus alerting provides a thorough performance engineering toolkit. By scripting realistic traffic scenarios, streaming real-time telemetry, and automating threshold gates in CI, teams can scale modern web applications with total confidence.
Regularly executing automated load tests against staging environments builds organizational muscle memory around capacity management. Infrastructure engineers and software developers share unified performance metrics, ensuring systems remain resilient under high traffic demands.
Maintaining automated load test scripts inside application repositories alongside feature code ensures that test scenarios evolve synchronously with API endpoints. Teams can safely optimize database indexing, update cache configurations, and refactor microservice interfaces with data-backed performance validation.
Establishing scheduled soak tests running for six to twelve hours surfaces subtle memory leaks, connection pool leaks, and unclosed file descriptor handles that short three-minute load tests miss. Automating soak tests in night-build environments provides detailed system health guarantees.
Using performance test reports during post-incident reviews creates transparency across infrastructure and development teams. Documenting throughput baselines before and after infrastructure migrations guarantees that architectural changes deliver real performance gains to end users.
Evaluating performance telemetry across sequential release versions helps engineering leads identify gradual performance degradation before system limits are breached in production environments.
You Might Also Like
- Microservices Contract Testing with Pact & Node.js
- Playwright E2E Testing: 4 Rules for Zero Flaky Tests
- Best Playwright Alternatives for Enterprise Automation
- Playwright vs Selenium for Enterprise Browser Automation
Test Your Knowledge
Frequently Asked Questions
What is Grafana k6 and why is it used for load testing?
Grafana k6 is a developer-centric, open-source load testing tool written in Go with JavaScript test scripting. It is designed for high performance and low resource consumption, enabling developers to simulate thousands of concurrent virtual users on minimal hardware while testing REST, GraphQL, and WebSocket APIs.
How does k6 stream real-time metrics to Grafana?
k6 streams telemetry directly to Prometheus or Grafana Mimir using the Prometheus Remote Write output exporter (-o experimental-prometheus-rw). Prometheus stores the metrics, allowing Grafana dashboards to plot real-time request throughput, error rates, and percentile latencies.
What is the difference between VUs and arrival rate in k6?
Virtual Users (VUs) represent concurrent execution threads simulating user sessions. Arrival rate (ramping-arrival-rate) specifies a fixed number of HTTP requests per second regardless of server response times, preventing slow backend responses from reducing load generator throughput.
How do k6 thresholds automate CI build failure gates?
k6 thresholds define performance pass and fail rules (such as p(95)<250ms or http_req_failed<1%). If any threshold is breached during execution, k6 exits with code 99, signaling CI tools like GitHub Actions or GitLab CI to fail the build automatically.
Can k6 tests be distributed across multiple machines?
Yes, k6 tests can be distributed across Kubernetes clusters using the k6-operator. The operator spawns parallel worker runner pods, splits load iterations automatically, and aggregates performance telemetry into central Prometheus and Grafana instances.
How do I pass custom authentication tokens into k6 scripts?
Custom authentication tokens can be passed into k6 scripts via environment variables (__ENV.AUTH_TOKEN) or generated dynamically inside the script using http.post() authentication calls before the main virtual user scenario function executes.
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

Prometheus Grafana Alerting and Burn Rate Design
Design production Prometheus and Grafana alerts using multi-window multi-burn-rate PromQL rules for service-level objectives (SLOs) and error budgets.
Read more
Playwright Visual Regression Testing at Scale: Docker Baselines, Pixelmatch & Flaky CI Shields
Comprehensive guide covering playwright visual regression testing at scale: docker baselines, pixelmatch & flaky ci shields with production-grade architecture and code examples.
Read more
Microservices Contract Testing with Pact & Node.js
Consumer-driven contract testing with Pact in Node.js microservices. Configure Pact Broker, provider state verification, and can-i-deploy in CI/CD.
Read more