http_reqs
Total HTTP requests and requests/second. This is your throughput indicator; a plateau as VUs rise can signal saturation.
A complete, line-by-line explanation of every core metric, percentile, and threshold—with a realistic report example and practical bottleneck-diagnosis tips.
A k6 report packs latency distributions, throughput, concurrency, error rates, and pass/fail verdicts into one summary. Misreading it is easy—and dangerous. A team can ship a broken release by looking only at average response time or overlooking a rising http_req_failed rate.
This guide explains the report zone by zone, shows how to read percentiles, and provides a disciplined way to decide whether a system is ready for real-world traffic.
k6 is an open-source, developer-centric load testing tool from Grafana Labs. JavaScript test scripts simulate virtual users (VUs) hitting an application concurrently while k6 measures how the system behaves.
When you run k6 run script.js, the final output has four useful zones: the execution header, functional checks, the metrics table, and thresholds.
Header describes execution mode, script, scenario, VUs, and duration. Checks are custom assertions. The metrics table contains timing, count, rate, and gauge data. Thresholds are the authoritative automated verdict.
Total HTTP requests and requests/second. This is your throughput indicator; a plateau as VUs rise can signal saturation.
Total request time: sending + waiting + receiving. Read its percentiles, not only its average.
Time to first byte (TTFB), usually the largest component. High waiting with low send/receive time points to backend processing.
Time waiting for a free client connection slot. High values can indicate connection-pool exhaustion.
TCP connection setup time, excluding TLS. High values suggest network or connection overhead.
HTTPS handshake time. Slow values can indicate poor reuse or an overloaded TLS terminator.
Time to send request data; typically relevant for large uploads.
Time to receive the response. Large bodies and slow delivery increase this value.
Percentage and count of failed requests. Read it alongside latency: a service can be fast because it fails fast.
Pass rate of custom assertions. A 200 response can still fail a body or business-logic check.
Time for a complete VU loop, including all requests, sleeps, and script processing.
Completed loop count and current/max virtual users. Compare concurrency with throughput to assess scaling.
Total network bytes transferred; useful for bandwidth planning and finding oversized payloads.
Duration of each group() block, useful for locating the slowest step in a user journey.
Never trust the average alone. Two systems can average 200ms while one has a painful long tail. Percentiles expose the experience of slower users.
| Measure | Meaning | Use it for |
|---|---|---|
| p50 (median) | Half of requests were faster. | Typical user experience |
| p90 | 90% were faster; 1 in 10 slower. | Broad tail monitoring |
| p95 | 95% were faster; 1 in 20 slower. | Common SLO target |
| p99 | 99% were faster; 1 in 100 slower. | High-scale tail risk |
| max | One worst-case sample. | Outlier investigation, not typical behavior |
At 10,000 requests per minute, a 3-second p99 still means roughly 100 requests per minute are that slow. “Rare” does not mean irrelevant at scale.
Thresholds turn measurements into an automated CI/CD decision. Define latency, error-rate, and functional criteria together:
export const options = {
thresholds: {
http_req_duration: ['p(95)<500'],
http_req_failed: ['rate<0.01'],
checks: ['rate>0.99'],
},
};Use a baseline before setting targets, keep safety margin below external SLAs, add tagged thresholds for critical endpoints, and consider abortOnFail for costly tests. A failed threshold makes the overall run fail—even if other metrics look healthy.
| Type | Examples | How to read it |
|---|---|---|
| Trend | http_req_duration | Focus on p95/p99, not just avg. |
| Counter | http_reqs, data_sent | Cumulative total and throughput rate. |
| Gauge | vus | Current value with min/max. |
| Rate | http_req_failed, checks | Percentage of true/non-zero events. |
http_req_failed with checks; protocol success does not guarantee correct content.http_reqs/s with VU levels and look for a plateau.Important: Never infer pass/fail from a metric value alone. The threshold expression, tags, abort settings, and actual ✓/✗ output are the source of truth.
http_req_failed because latency is low.k6 run --out json=results.json script.js for raw measurements.95% of measured requests completed faster than the stated value; 5% were slower.
Duration covers sending, waiting, and receiving. Waiting is the server-response/TTFB portion. Their difference helps locate network versus backend time.
Request failure is protocol-level; checks are custom assertions. A 200 response can pass the former and fail the latter.
Use a percentile—typically p95 or p99—because averages hide tail latency.
A step-by-step guide from setup to execution—combining functional browser journeys with realistic performance testing.
Modern web applications must be fast and functionally correct under real-world traffic. Playwright is excellent for end-to-end verification, while k6 is purpose-built for load and performance testing. K6’s experimental browser module provides a Playwright-inspired API for browser-level performance checks.
| Playwright | k6 | |
|---|---|---|
| Primary purpose | Functional and E2E testing | Load and performance testing |
| Execution model | Real browser, single/few sessions | Thousands of VUs at protocol level, or smaller browser-level loads |
| Scripting | JavaScript/TypeScript, Python, Java, .NET | JavaScript (Goja runtime) |
| Metrics | Pass/fail assertions | Latency, throughput, error rate, thresholds |
| Browser API | Native | k6/experimental/browser, Playwright-inspired |
Pairing the tools lets teams reuse selectors and user flows, validate critical journeys such as login or checkout, capture browser metrics alongside load metrics, and run tests from CI/CD.
You’ll need Node.js v16 or later with npm, a code editor, optional Git, basic JavaScript and selector knowledge, and administrator or sudo access for k6 and browser dependencies.
mkdir k6-playwright-perf-test
cd k6-playwright-perf-test
npm init -ynpm install -D @playwright/test
npx playwright install --with-depsKeep a Playwright functional suite in the repository so the journey can be verified before it is performance-tested.
# macOS
brew install k6
# Windows
choco install k6
# Debian / Ubuntu
sudo apt-get update
sudo apt-get install k6
# Docker alternative
docker pull grafana/k6
k6 versionModern k6 releases bundle the experimental browser module. A recent release such as k6 v0.47.0 or later is recommended. Older releases may require an xk6 custom build:
go install go.k6.io/xk6/cmd/xk6@latest
xk6 build --with github.com/grafana/xk6-browser@latestk6-playwright-perf-test/
├── playwright-tests/
│ └── login.spec.js
├── k6-tests/
│ └── login.perf.js
├── package.json
└── playwright.config.jsFirst validate the login flow functionally, then translate the same selectors and steps into the k6 script.
// playwright-tests/login.spec.js
const { test, expect } = require('@playwright/test');
test('user can log in', async ({ page }) => {
await page.goto('https://test.k6.io/my_messages.php');
await page.fill('#login', 'admin');
await page.fill('#password', '123');
await page.click('input[type="submit"]');
await expect(page.locator('h3')).toContainText('Welcome, admin!');
});npx playwright test playwright-tests/login.spec.jsimport { browser } from 'k6/experimental/browser';
import { check } from 'k6';
export const options = {
scenarios: {
ui: {
executor: 'shared-iterations',
vus: 5,
iterations: 10,
options: { browser: { type: 'chromium' } },
},
},
thresholds: {
checks: ['rate==1.0'],
browser_http_req_duration: ['p(95)<2000'],
},
};
export default async function () {
const page = browser.newPage();
try {
await page.goto('https://test.k6.io/my_messages.php');
await page.locator('#login').type('admin');
await page.locator('#password').type('123');
await Promise.all([
page.waitForNavigation(),
page.locator('input[type="submit"]').click(),
]);
const heading = page.locator('h3');
check(heading, {
'welcome message is visible': (h) =>
h.textContent().includes('Welcome, admin!'),
});
} finally {
page.close();
}
}Key concepts: browser.newPage() creates a browser page, page.locator() mirrors Playwright’s selector model, scenarios define the load shape, thresholds define objective pass/fail criteria, and check() records assertions without acting like a test-runner exception.
export const options = {
scenarios: {
ui: {
executor: 'ramping-vus',
startVUs: 1,
stages: [
{ duration: '30s', target: 5 },
{ duration: '1m', target: 5 },
{ duration: '30s', target: 0 },
],
options: { browser: { type: 'chromium' } },
},
},
thresholds: {
browser_http_req_duration: ['p(90)<1500', 'p(95)<2500'],
checks: ['rate>0.95'],
},
};k6 run k6-tests/login.perf.js
# Docker
docker run --rm -i -v $(pwd):/scripts grafana/k6 \
run /scripts/k6-tests/login.perf.js
# JSON output
k6 run --out json=results.json k6-tests/login.perf.js
# InfluxDB output
k6 run --out influxdb=http://localhost:8086/k6 k6-tests/login.perf.jschecks.........................: 100.00% ✓ 10 ✗ 0
browser_http_req_duration......: avg=812ms p(95)=1.3s
browser_web_vital_lcp...........: avg=1.1s
iteration_duration..............: avg=2.3s
vus..............................: 5check() assertions.Combining k6 with a Playwright-style browser API provides a consistent way to validate functional correctness and real-world performance. Verify the flow with Playwright, translate it to a k6 browser script, define a realistic scenario and thresholds, execute it, and use the resulting metrics to catch regressions early.
No. It is a Playwright-inspired API designed for performance testing in k6. Playwright remains the dedicated functional and E2E tool.
Browser-level VUs are resource-heavy. Use protocol-level k6 tests for high-concurrency load and browser-level tests for smaller, representative populations.
No. k6 runs its browser module independently. Playwright is complementary and useful for validating flows and reusing selectors.
Yes. Both tools are CLI-driven and return process exit codes suitable for GitHub Actions, GitLab CI, Jenkins, and similar systems.