Performance-Testing ist kein Nice-to-have mehr — es ist ein kritischer Bestandteil moderner API-Entwicklung. k6, das Open-Source-Lasttesttool von Grafana Labs, kombiniert Entwicklerfreundlichkeit mit ernsthafter Power: TypeScript-nahe Syntax, präzise Metriken, flexible Szenarien und nahtlose CI/CD-Integration. In Kombination mit Claude Code als KI-Pair-Programmer entstehen Lasttests, die früher Stunden dauerten, in Minuten.

In diesem Guide zeigen wir, wie du k6 von den Grundlagen bis zur vollständigen Grafana-Cloud-Integration nutzt — mit realistischen Codebeispielen, die du sofort übernehmen kannst.

Voraussetzungen: k6 installiert (brew install k6 oder snap install k6), Node.js für TypeScript-Projekte, optional Docker für lokalen Grafana-Stack.

1. k6 Grundlagen & erster Test

k6-Tests sind JavaScript-Dateien mit einer Standard-Export-Funktion. Der Scheduler ruft diese Funktion für jeden Virtual User (VU) in einer Schleife auf. Das Grundprinzip ist denkbar einfach — aber die Tiefe steckt in den Details.

Grundlagen

Erster HTTP-Test mit Checks und Metriken

import http from 'k6/http'; import { check, sleep } from 'k6'; import { Trend, Rate, Counter } from 'k6/metrics'; // Eigene Metriken definieren const loginDuration = new Trend('login_duration', true); const errorRate = new Rate('error_rate'); const requestCount = new Counter('total_requests'); // Konfiguration: 20 VUs für 30 Sekunden export const options = { vus: 20, duration: '30s', thresholds: { http_req_duration: ['p(95)<500'], error_rate: ['rate<0.01'], }, }; export default function () { // Einfacher GET-Request const res = http.get('https://api.example.com/v1/health'); requestCount.add(1); // Checks definieren — beschreibende Labels sind Pflicht const ok = check(res, { 'status ist 200': (r) => r.status === 200, 'antwortzeit unter 500ms': (r) => r.timings.duration < 500, 'body enthält success': (r) => r.body.includes('success'), }); // Fehlerrate tracken errorRate.add(!ok); // Denk-Pause simulieren (realistisches Nutzerverhalten) sleep(1); }

Claude Code hilft hier besonders beim Aufsetzen der initialen Teststruktur. Prompt: "Erstelle einen k6-Lasttest für unsere REST-API mit sinnvollen Thresholds für eine E-Commerce-Plattform." — Claude Code generiert sofort produktionsreife Metriken und Checks.

Metriken-Übersicht

Eingebaute k6 Metriken verstehen

MetrikTypBeschreibung
http_req_durationTrendGesamte Request-Dauer inkl. Warten
http_req_waitingTrendTime to First Byte (TTFB)
http_req_connectingTrendTCP-Verbindungsaufbau
http_req_failedRateAnteil fehlgeschlagener Requests
http_reqsCounterGesamtzahl aller Requests
vusGaugeAktuell aktive Virtual Users
iterationsCounterAnzahl abgeschlossener Iterationen
data_receivedCounterEmpfangene Datenmenge in Bytes

Der erste Test läuft mit k6 run mein-test.js. Die Ausgabe zeigt nach dem Testlauf alle Metriken übersichtlich — Percentile, Minimum, Maximum, Median und Durchschnitt auf einen Blick.

Claude Code Tipp: Lass Claude Code deine bestehende OpenAPI/Swagger-Spezifikation analysieren und daraus einen vollständigen k6-Test mit realistischen Payloads generieren. Das spart 80% der manuellen Arbeit beim Test-Setup.

2. Scenarios & Load-Profile

Reale Last ist nie gleichmäßig. Mit k6 Scenarios kannst du exakt das Verhalten echter Nutzer modellieren: langsames Hochfahren, Lastspitzen, konstante Anfragenrate und parallele Test-Flows in einem einzigen Test-Run.

Scenarios

Drei Executors für drei Lastprofile

import http from 'k6/http'; import { sleep } from 'k6'; export const options = { scenarios: { // 1. Ramping VUs: Sanfter Warmup → Peak → Abkühlung warmup_and_peak: { executor: 'ramping-vus', startVUs: 0, stages: [ { duration: '2m', target: 50 }, // Hochfahren { duration: '5m', target: 50 }, // Konstante Last { duration: '2m', target: 200 }, // Lastspitze { duration: '1m', target: 0 }, // Abkühlung ], gracefulRampDown: '30s', tags: { scenario: 'warmup' }, }, // 2. Constant Arrival Rate: Exakt N Requests/Sekunde steady_load: { executor: 'constant-arrival-rate', rate: 100, // 100 Requests/Sekunde timeUnit: '1s', duration: '5m', preAllocatedVUs: 50, maxVUs: 200, startTime: '2m', // Startet nach 2 Minuten tags: { scenario: 'steady' }, }, // 3. Ramping Arrival Rate: Wachsende Request-Rate growing_traffic: { executor: 'ramping-arrival-rate', startRate: 10, timeUnit: '1s', preAllocatedVUs: 100, maxVUs: 500, stages: [ { duration: '3m', target: 50 }, { duration: '3m', target: 200 }, { duration: '2m', target: 50 }, ], startTime: '5m', tags: { scenario: 'growing' }, }, }, }; // Szenarien können auf unterschiedliche exec-Funktionen zeigen export function warmup_and_peak() { http.get('https://api.example.com/v1/products'); sleep(Math.random() * 2 + 0.5); // 0.5–2.5s Pause } export function steady_load() { http.get('https://api.example.com/v1/search?q=test'); } export function growing_traffic() { http.post('https://api.example.com/v1/orders', JSON.stringify({ productId: 42, qty: 1 }), { headers: { 'Content-Type': 'application/json' } } ); } // Default Export als Fallback export default function () { warmup_and_peak(); }
Executor-Typen

Wann welchen Executor verwenden?

ExecutorKontrolliertUse Case
shared-iterationsGesamtanzahl IterationenFester Testumfang
per-vu-iterationsIterationen pro VUReproduzierbare Tests
constant-vusParallele NutzerEinfache Basistests
ramping-vusVU-Kurve über ZeitRealistische Lastprofile
constant-arrival-rateRequests/ZeiteinheitSLA-Tests, API-Rate-Limits
ramping-arrival-rateWachsende Request-RateBlack Friday, Produktlaunches
externally-controlledManuell via k6 REST APIInteraktive Tests

Claude Code eignet sich hervorragend, um aus Deployment-Metriken (z.B. aus Datadog oder Grafana) automatisch realistische Stage-Profile zu generieren. Einfach die Anfragekurven beschreiben und Claude Code schreibt die passenden Stages.

3. Thresholds & Checks

Thresholds sind das Herzstück der automatisierten Performance-Validierung. Sie definieren, wann ein Test als "bestanden" oder "fehlgeschlagen" gilt — und damit, ob deine CI/CD-Pipeline blockt oder durchläuft.

Thresholds

Umfassendes Threshold-Setup für Produktions-APIs

import http from 'k6/http'; import { check, group } from 'k6'; import { Rate, Trend } from 'k6/metrics'; const criticalErrors = new Rate('critical_errors'); const checkoutLatency = new Trend('checkout_latency', true); export const options = { vus: 100, duration: '5m', thresholds: { // Allgemeine HTTP-Latenz 'http_req_duration': [ 'p(50)<200', // Median unter 200ms 'p(95)<800', // 95% unter 800ms 'p(99)<2000', // 99% unter 2 Sekunden 'max<5000', // Kein einzelner Request über 5s ], // Fehlerrate: unter 1% 'http_req_failed': ['rate<0.01'], // Kritische Fehler: unter 0.1% 'critical_errors': [ { threshold: 'rate<0.001', abortOnFail: true } // Test sofort abbrechen! ], // Endpoint-spezifische Thresholds (per Tag) 'http_req_duration{endpoint:checkout}': ['p(95)<3000'], 'http_req_duration{endpoint:search}': ['p(95)<500'], 'http_req_duration{endpoint:auth}': ['p(95)<1000'], // Custom Checkout-Latenz 'checkout_latency': ['p(95)<2500', 'avg<1500'], // Check-Success-Rate 'checks': ['rate>0.99'], // 99% aller Checks grün }, }; export default function () { group('Produkt-Suche', () => { const res = http.get( 'https://api.example.com/v1/search?q=laptop', { tags: { endpoint: 'search' } } ); check(res, { 'Suche: Status 200': (r) => r.status === 200, 'Suche: Ergebnisse vorhanden': (r) => { const body = JSON.parse(r.body); return body.results && body.results.length > 0; }, 'Suche: Korrektes Content-Type': (r) => r.headers['Content-Type'].includes('application/json'), }); }); group('Checkout-Flow', () => { const start = Date.now(); const res = http.post( 'https://api.example.com/v1/checkout', JSON.stringify({ cartId: 'test-cart-123', paymentToken: 'tok_test' }), { headers: { 'Content-Type': 'application/json' }, tags: { endpoint: 'checkout' }, } ); checkoutLatency.add(Date.now() - start); const isCriticalError = res.status === 500 || res.status === 503; criticalErrors.add(isCriticalError); check(res, { 'Checkout: Kein 5xx Fehler': (r) => r.status < 500, 'Checkout: Order-ID vorhanden': (r) => { if (r.status !== 200 && r.status !== 201) return false; const body = JSON.parse(r.body); return !!body.orderId; }, }); }); }
abortOnFail: Wenn kritische Fehler die 0,1%-Schwelle überschreiten, bricht k6 den Test sofort ab. Das verhindert unnötige Last auf einem bereits überlasteten System — besonders wichtig in Produktionsumgebungen.

Mit Claude Code kannst du Thresholds aus bestehenden SLAs automatisch generieren lassen. Gib einfach deine Service Level Objectives an — Claude Code übersetzt sie in k6-Threshold-Syntax und ergänzt sinnvolle abortOnFail-Regeln für kritische Pfade.

4. Authentication & Komplexe Flows

Realistische Lasttests erfordern authentifizierte Requests, Multi-Step-Flows und gemeinsam genutzte Testdaten. k6 bietet dafür mächtige Primitiven: setup() für einmalige Initialisierung, SharedArray für speichereffiziente Testdaten und batch() für parallele Requests.

Auth & Flows

Bearer Token, Session-Flow und parallele Requests

import http from 'k6/http'; import { check, sleep } from 'k6'; import { SharedArray } from 'k6/data'; // SharedArray: Daten einmal laden, effizient teilen const users = new SharedArray('test-users', () => { return JSON.parse(open('./testdata/users.json')); }); const BASE_URL = 'https://api.example.com'; // setup(): Läuft einmal vor allen VUs export function setup() { // Admin-Token holen für Setup-Aufgaben const res = http.post(`${BASE_URL}/auth/token`, { grant_type: 'client_credentials', client_id: __ENV.CLIENT_ID, client_secret: __ENV.CLIENT_SECRET, }); check(res, { 'Setup: Token erhalten': (r) => r.status === 200 }); return { adminToken: JSON.parse(res.body).access_token }; } export default function (setupData) { // Zufälligen Testnutzer wählen const user = users[Math.floor(Math.random() * users.length)]; // Schritt 1: Login mit Formdata const loginRes = http.post( `${BASE_URL}/auth/login`, // FormData für klassisches Login-Form { email: user.email, password: user.password }, { tags: { step: 'login' } } ); check(loginRes, { 'Login: Status 200': (r) => r.status === 200, 'Login: Token vorhanden': (r) => !!JSON.parse(r.body).token, }); const token = JSON.parse(loginRes.body).token; const authHeaders = { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }; sleep(0.5); // Kurze Pause nach Login // Schritt 2: Parallele Batch-Requests const responses = http.batch([ ['GET', `${BASE_URL}/v1/user/profile`, null, { headers: authHeaders }], ['GET', `${BASE_URL}/v1/user/orders?limit=10`, null, { headers: authHeaders }], ['GET', `${BASE_URL}/v1/recommendations`, null, { headers: authHeaders }], ]); check(responses[0], { 'Profil: geladen': (r) => r.status === 200 }); check(responses[1], { 'Orders: geladen': (r) => r.status === 200 }); check(responses[2], { 'Empfehlungen: geladen': (r) => r.status === 200 }); sleep(1); // Schritt 3: Komplexer POST mit JSON-Body const orderRes = http.post( `${BASE_URL}/v1/cart/checkout`, JSON.stringify({ items: [{ productId: user.favoriteProduct, quantity: 1 }], shippingAddress: user.address, paymentMethod: 'test_card', }), { headers: authHeaders, tags: { step: 'checkout' } } ); check(orderRes, { 'Checkout: erfolgreich': (r) => r.status === 201, 'Checkout: Order-ID in Response': (r) => !!JSON.parse(r.body).orderId, }); sleep(Math.random() * 3 + 1); // 1–4s realistische Pause } // teardown(): Aufräumen nach dem Test export function teardown(setupData) { // Admin-Token invalidieren http.post(`${BASE_URL}/auth/revoke`, null, { headers: { 'Authorization': `Bearer ${setupData.adminToken}` } }); }

Claude Code ist beim Erstellen komplexer Flows besonders nützlich: Es kann bestehende Playwright-E2E-Tests analysieren und daraus k6-Lasttests generieren — inklusive realistischer Pausen, Fehlerbehandlung und SharedArray-Optimierungen für große Testdaten-Mengen.

Umgebungsvariablen

Konfiguration über CLI-Flags

# Basis-URL und Credentials aus Environment k6 run \ -e BASE_URL=https://staging.api.example.com \ -e CLIENT_ID=test-client \ -e CLIENT_SECRET=super-secret \ --vus 50 \ --duration 10m \ mein-test.js # Oder aus .env File laden k6 run --env-file=.env mein-test.js

5. Grafana k6 Cloud & Dashboards

Lokale Testergebnisse reichen für kleine Teams. Für größere Lasttests, Trendanalysen und Team-Dashboards ist die Grafana k6 Cloud oder ein lokaler InfluxDB/Grafana-Stack die bessere Wahl. Claude Code hilft beim Aufsetzen beider Optionen.

Cloud

k6 Cloud und lokaler InfluxDB-Stack

# Option 1: k6 Cloud (Grafana Cloud) # Login einmalig k6 cloud login --token $K6_CLOUD_TOKEN # Test in der Cloud ausführen k6 cloud mein-test.js # Lokal ausführen, Ergebnisse an Cloud streamen k6 run --out cloud mein-test.js # Option 2: Lokaler InfluxDB + Grafana Stack (Docker) # docker-compose.yml für lokales Monitoring: # services: # influxdb: # image: influxdb:2.7 # ports: ["8086:8086"] # grafana: # image: grafana/grafana:latest # ports: ["3000:3000"] # k6 mit InfluxDB-Output k6 run --out influxdb=http://localhost:8086/k6 mein-test.js # Oder mit Prometheus Remote Write k6 run --out experimental-prometheus-rw mein-test.js # Multiple Outputs gleichzeitig k6 run \ --out influxdb=http://localhost:8086/k6 \ --out json=results.json \ mein-test.js
Grafana

k6-spezifische Metriken in Grafana Dashboard

// k6-Testdatei mit Cloud-Konfiguration import http from 'k6/http'; import { sleep } from 'k6'; export const options = { // Cloud-Metadaten für Dashboard-Organisation ext: { loadimpact: { projectID: 3589057, name: 'API Performance Test KW19', distribution: { // Lastverteilung auf verschiedene Cloud-Regionen 'amazon:de:frankfurt': { loadZone: 'amazon:de:frankfurt', percent: 60 }, 'amazon:us:portland': { loadZone: 'amazon:us:portland', percent: 40 }, }, }, }, scenarios: { load_test: { executor: 'ramping-vus', startVUs: 0, stages: [ { duration: '5m', target: 500 }, { duration: '10m', target: 500 }, { duration: '5m', target: 0 }, ], }, }, thresholds: { http_req_duration: ['p(95)<1000'], http_req_failed: ['rate<0.005'], }, }; export default function () { // Tags werden in Grafana als Dimensionen sichtbar const res = http.get('https://api.example.com/v1/products', { tags: { service: 'product-api', version: 'v1', region: __ENV.REGION || 'unknown', }, }); sleep(1); }
Grafana Dashboard-Import: Das offizielle k6-Dashboard für Grafana (ID: 2587) zeigt alle Standard-Metriken sofort nach InfluxDB-Verbindung. Claude Code kann dir ein custom Dashboard-JSON generieren, das deine spezifischen Business-Metriken visualisiert.

Real-time-Metriken während eines laufenden Tests sind der entscheidende Vorteil des Grafana-Stacks: Du siehst sofort, wann ein Threshold überschritten wird, und kannst den Test bei Bedarf manuell abbrechen — bevor das Produktionssystem in die Knie geht.

Metriken-Tags

Tag-Strategie für sinnvolle Grafana-Filterung

TagWert-BeispieleGrafana-Filter
endpointauth, search, checkoutEndpoint-spezifische SLA-Übersicht
serviceproduct-api, payment-apiService-Vergleich
scenariowarmup, peak, steadyLastprofil-Analyse
envstaging, prodUmgebungsvergleich
versionv1, v2, canaryAPI-Version-Rollout

6. CI/CD Integration & Browser Testing

k6 in der CI/CD-Pipeline macht Performance-Regressionen sichtbar, bevor sie in Produktion gelangen. GitHub Actions, GitLab CI und Jenkins werden alle unterstützt. Das Browser-Testing-Modul erweitert k6 um echte Browser-Interaktionen mit Chromium.

GitHub Actions

Vollständige CI/CD-Pipeline mit k6 Action

# .github/workflows/performance.yml name: Performance Tests on: push: branches: [main, develop] pull_request: branches: [main] schedule: # Täglich um 02:00 UTC (nächtlicher Regressionstest) - cron: '0 2 * * *' jobs: k6-load-test: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: k6 Load Test ausführen uses: grafana/k6-action@v0.3.1 with: filename: tests/performance/load-test.js flags: > --vus 50 --duration 5m --out json=results/k6-results.json env: K6_CLOUD_TOKEN: ${{ secrets.K6_CLOUD_TOKEN }} BASE_URL: ${{ vars.STAGING_URL }} CLIENT_SECRET: ${{ secrets.API_CLIENT_SECRET }} - name: Threshold-Ergebnis prüfen # k6 gibt Exit-Code 99 bei Threshold-Fehler run: | if [ $? -eq 99 ]; then echo "::error::k6 Thresholds überschritten! Performance-Regression erkannt." exit 1 fi - name: Ergebnisse als Artefakt speichern uses: actions/upload-artifact@v4 if: always() with: name: k6-performance-results path: results/ retention-days: 30 - name: PR-Kommentar mit Ergebnissen if: github.event_name == 'pull_request' uses: actions/github-script@v7 with: script: | const fs = require('fs'); const results = JSON.parse(fs.readFileSync('results/summary.json', 'utf8')); const p95 = results.metrics.http_req_duration.values['p(95)']; const errorRate = results.metrics.http_req_failed.values.rate; github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: [ '## k6 Performance-Ergebnisse', `- **p95 Latenz:** ${p95.toFixed(0)}ms`, `- **Fehlerrate:** ${(errorRate * 100).toFixed(3)}%`, `- **Status:** ${p95 < 800 ? '✅ OK' : '❌ Threshold überschritten'}`, ].join('\n') });
Browser Testing

k6 Browser API mit Chromium

import { browser } from 'k6/browser'; import { check, sleep } from 'k6'; import { Trend } from 'k6/metrics'; const pageLoadTime = new Trend('page_load_time', true); const lcp = new Trend('largest_contentful_paint', true); export const options = { scenarios: { // Browser-Szenario mit Chromium ui_test: { executor: 'constant-vus', vus: 5, // Browser-Tests: weniger VUs! duration: '3m', options: { browser: { type: 'chromium', }, }, }, }, thresholds: { page_load_time: ['p(95)<3000'], largest_contentful_paint: ['p(75)<2500'], // Core Web Vital }, }; export default async function () { const page = await browser.newPage(); try { // Produktseite laden und LCP messen const startTime = Date.now(); await page.goto('https://example.com/products/laptop-pro'); await page.waitForLoadState('networkidle'); pageLoadTime.add(Date.now() - startTime); // Core Web Vital: LCP aus Browser-API const lcpValue = await page.evaluate(() => { return new Promise((resolve) => { new PerformanceObserver((list) => { const entries = list.getEntries(); resolve(entries[entries.length - 1].startTime); }).observe({ type: 'largest-contentful-paint', buffered: true }); }); }); lcp.add(lcpValue); // "In den Warenkorb" Button klicken await page.locator('[data-testid="add-to-cart"]').click(); await page.waitForSelector('[data-testid="cart-notification"]'); check(page, { 'Warenkorb-Benachrichtigung sichtbar': (p) => p.locator('[data-testid="cart-notification"]').isVisible(), }); // Screenshot bei Fehler await page.screenshot({ path: `screenshots/product-${Date.now()}.png` }); } finally { await page.close(); } sleep(2); }
JSON Summary

handleSummary() für Custom-Reports

import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.2/index.js'; export function handleSummary(data) { // Mehrere Output-Formate gleichzeitig return { // Standard Terminal-Output 'stdout': textSummary(data, { indent: ' ', enableColors: true }), // JSON für CI-Auswertung 'results/summary.json': JSON.stringify(data, null, 2), // HTML-Report für Artefakt-Upload 'results/report.html': generateHtmlReport(data), }; } function generateHtmlReport(data) { const p95 = data.metrics.http_req_duration.values['p(95)']; const passed = p95 < 800; return `<!DOCTYPE html> <html><head><title>k6 Report</title></head> <body style="font-family:sans-serif;padding:24px"> <h1 style="color:${passed?'#22c55e':'#ef4444'}"> ${passed ? '✅ Performance OK' : '❌ Threshold überschritten'} </h1> <p>p95 Latenz: <strong>${p95.toFixed(0)}ms</strong></p> </body></html>`; }
Claude Code + k6 in CI: Lass Claude Code deine historischen Performance-Daten analysieren und automatisch adaptive Thresholds vorschlagen — basierend auf Percentile-Trends der letzten 30 Test-Runs. So werden Thresholds realistisch statt willkürlich.

Mit dieser Setup-Kombination — k6 Scenarios für realistische Last, präzise Thresholds als Quality Gates, Grafana für Echtzeit-Monitoring und GitHub Actions für automatische CI/CD-Integration — hast du eine vollständige Performance-Testing-Pipeline, die Regressionen findet, bevor Nutzer sie spüren.

Fazit: k6 + Claude Code als Performance-Duo

k6 ist das leistungsfähigste Open-Source-Lasttesttool der aktuellen Generation — und Claude Code macht es zugänglicher als je zuvor. Die wichtigsten Erkenntnisse aus diesem Guide:

Claude Code beschleunigt dabei jeden Schritt: Test-Generierung aus OpenAPI-Specs, Threshold-Kalkulation aus SLAs, Dashboard-JSON aus Metrik-Anforderungen und Incident-Reports aus Testresultaten. Performance Testing ist damit kein Spezialistenwissen mehr — sondern Standard im Entwicklungs-Workflow.

Testing-Modul im Kurs

Im Claude Code Mastery Kurs: vollständiges k6-Modul mit Scenarios, Thresholds, Grafana-Integration und CI/CD-Pipeline für systematisches API-Performance-Testing.

14 Tage kostenlos testen →