Lasttests für deine API: Tools, Muster und Fallstricke
Praktischer Leitfaden für API-Lasttests: Tool-Auswahl, realistische Traffic-Muster, Performance-Baselines, Engpässe und häufige Fehler.

Lasttests beantworten eine einfache Frage: Was passiert, wenn echter Traffic auf dein System trifft? Nicht die theoretische Kapazität, sondern das tatsächliche Verhalten unter Last. Die meisten Performance-Probleme sind bei geringem Traffic unsichtbar und bei hohem Traffic katastrophal. Lasttests decken sie auf, bevor es deine Nutzer tun.
Die Herausforderung besteht nicht darin, einen Lasttest durchzuführen. Die Herausforderung besteht darin, einen Test zu entwerfen, der aussagekräftige Ergebnisse liefert. Einen Endpunkt mit 10.000 Anfragen pro Sekunde zu bombardieren, sagt dir etwas – aber wahrscheinlich nicht das, was du wissen musst.
Das richtige Tool auswählen
Unterschiedliche Tools passen zu unterschiedlichen Anforderungen. Das beste Tool ist das, das dein Team tatsächlich benutzt.
// k6 — JavaScript-based, good for developers
// Install: brew install k6
// Run: k6 run loadtest.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 50 }, // Ramp up to 50 users
{ duration: '5m', target: 50 }, // Stay at 50 users
{ duration: '2m', target: 200 }, // Ramp up to 200 users
{ duration: '5m', target: 200 }, // Stay at 200 users
{ duration: '2m', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% of requests under 500ms
http_req_failed: ['rate<0.01'], // Less than 1% error rate
},
};
export default function () {
const res = http.get('https://api.example.com/products');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
'has products': (r) => JSON.parse(r.body).length > 0,
});
sleep(1); // Simulate user think time
}Der entscheidende Unterschied zu anderen Tools: k6-Skripte sind echtes JavaScript. Du kannst Variablen, Schleifen, Bedingungen und gemeinsam genutzten Zustand verwenden. Tests lesen sich wie Anwendungscode, nicht wie XML-Konfiguration.
Realistische Traffic-Muster
Der größte Fehler bei Lasttests: gleichförmigen Traffic an einen einzigen Endpunkt zu senden.
// ❌ Unrealistic — all requests hit the same endpoint at the same rate
export default function () {
http.get('https://api.example.com/products');
}
// Real users don't do this. Your cache warms up for one endpoint
// while the rest of the system is untested.// ✅ Realistic — model actual user behavior
import http from 'k6/http';
import { sleep, group } from 'k6';
const BASE_URL = 'https://api.example.com';
export default function () {
// 60% of traffic: browse products
group('browse', () => {
http.get(`${BASE_URL}/products`);
sleep(2);
const productId = Math.floor(Math.random() * 1000) + 1;
http.get(`${BASE_URL}/products/${productId}`);
sleep(3);
});
// 25% of traffic: search
if (Math.random() < 0.25) {
group('search', () => {
const queries = ['laptop', 'headphones', 'keyboard', 'monitor', 'mouse'];
const query = queries[Math.floor(Math.random() * queries.length)];
http.get(`${BASE_URL}/search?q=${query}`);
sleep(2);
});
}
// 10% of traffic: add to cart and checkout
if (Math.random() < 0.10) {
group('checkout', () => {
const payload = JSON.stringify({
productId: Math.floor(Math.random() * 1000) + 1,
quantity: 1,
});
http.post(`${BASE_URL}/cart`, payload, {
headers: { 'Content-Type': 'application/json' },
});
sleep(1);
http.post(`${BASE_URL}/checkout`, '{}', {
headers: { 'Content-Type': 'application/json' },
});
});
}
// 5% of traffic: authenticated user actions
if (Math.random() < 0.05) {
group('account', () => {
http.get(`${BASE_URL}/account/orders`, {
headers: { 'Authorization': `Bearer ${getTestToken()}` },
});
});
}
}
function getTestToken(): string {
// Use pre-generated test tokens — don't hit the auth endpoint in load tests
const tokens = ['token-1', 'token-2', 'token-3'];
return tokens[Math.floor(Math.random() * tokens.length)];
}Die Traffic-Verteilung (60 % Stöbern, 25 % Suche, 10 % Checkout, 5 % Konto) sollte deinen Produktions-Analysen entsprechen. Wenn nur 1 % des echten Traffics auf den Checkout entfällt, liefert ein Test des Checkouts mit 50 % Last bedeutungslose Ergebnisse.
Baselines festlegen
Bevor du Lasttests durchführst, lege Baselines mit einzelnen Anfragen fest. Du musst wissen, wie „normal" aussieht, bevor du „degradiert" erkennen kannst.
// baseline.js — run with: k6 run --vus 1 --iterations 100 baseline.js
import http from 'k6/http';
import { check } from 'k6';
import { Trend } from 'k6/metrics';
const productListDuration = new Trend('product_list_duration');
const productDetailDuration = new Trend('product_detail_duration');
const searchDuration = new Trend('search_duration');
const checkoutDuration = new Trend('checkout_duration');
export default function () {
// Product list
let res = http.get('https://api.example.com/products');
productListDuration.add(res.timings.duration);
check(res, { 'product list 200': (r) => r.status === 200 });
// Product detail
res = http.get('https://api.example.com/products/1');
productDetailDuration.add(res.timings.duration);
check(res, { 'product detail 200': (r) => r.status === 200 });
// Search
res = http.get('https://api.example.com/search?q=laptop');
searchDuration.add(res.timings.duration);
check(res, { 'search 200': (r) => r.status === 200 });
}## Expected output format:
product_list_duration....: avg=45ms p(95)=78ms p(99)=120ms
product_detail_duration..: avg=12ms p(95)=25ms p(99)=45ms
search_duration..........: avg=95ms p(95)=180ms p(99)=310ms
## Now you know:
## - Product list: baseline P95 is 78ms
## - If load test shows P95 at 400ms, that's 5x degradation
## - Search at 180ms P95 is already slow — investigate before load testing
Engpässe identifizieren
Lasttest-Ergebnisse weisen auf Engpässe hin. Die Muster sind vorhersehbar.
## Bottleneck Patterns
### Latency increases linearly with load
- Cause: CPU-bound processing (no concurrency)
- Fix: Profile the hot path, optimize or parallelize
### Latency is stable until a threshold, then spikes
- Cause: Resource exhaustion (connection pool, thread pool, memory)
- Fix: Increase pool size, add horizontal scaling
### Error rate increases with load
- Cause: Timeout or circuit breaker tripping
- Fix: Increase timeout, add retry with backoff, scale backend
### First request is slow, subsequent are fast
- Cause: Cold cache, JIT compilation, connection establishment
- Fix: Warm caches on deploy, use connection pooling// k6 custom metrics for bottleneck identification
import { Counter, Trend } from 'k6/metrics';
const dbQueryDuration = new Trend('db_query_duration');
const cacheHitRate = new Counter('cache_hits');
const cacheMissRate = new Counter('cache_misses');
export default function () {
const res = http.get('https://api.example.com/products');
// Parse custom headers that expose server timing
const serverTiming = res.headers['Server-Timing'];
if (serverTiming) {
// Server-Timing: db;dur=45, cache;desc="miss"
const dbMatch = serverTiming.match(/db;dur=(\d+)/);
if (dbMatch) {
dbQueryDuration.add(parseInt(dbMatch[1]));
}
if (serverTiming.includes('cache;desc="hit"')) {
cacheHitRate.add(1);
} else {
cacheMissRate.add(1);
}
}
}Stelle in Staging-Umgebungen Server-Timing-Header von deiner API bereit. Damit kannst du die Gesamtlatenz in Datenbankzeit, Cache-Abfragen und Anwendungsverarbeitung aufschlüsseln, ohne den Lasttest-Client instrumentieren zu müssen.
Häufige Fehler
// ❌ Mistake 1: Testing against production
// Affects real users, skews analytics, risks data corruption
// ❌ Mistake 2: No warm-up period
export const options = {
vus: 1000, // Immediate spike — not representative of real traffic
duration: '30s',
};
// ✅ Always ramp up gradually
export const options = {
stages: [
{ duration: '5m', target: 100 },
{ duration: '10m', target: 100 },
{ duration: '5m', target: 0 },
],
};
// ❌ Mistake 3: Ignoring think time
export default function () {
http.get(url); // Fires as fast as possible
// 50 VUs without sleep = thousands of RPS
// 50 real users make maybe 1 request per 3 seconds each
}
// ✅ Include realistic think time
export default function () {
http.get(url);
sleep(Math.random() * 3 + 1); // 1-4 seconds between actions
}
// ❌ Mistake 4: Only looking at averages
// "Average response time: 200ms" hides that P99 is 8 seconds
// ✅ Always check percentiles
export const options = {
thresholds: {
http_req_duration: [
'p(50)<200', // Median under 200ms
'p(95)<500', // 95th percentile under 500ms
'p(99)<2000', // 99th percentile under 2 seconds
],
},
};Die wichtigsten Erkenntnisse
- Realistischen Traffic modellieren — bilde deine Produktions-Traffic-Verteilung ab, statt gleichförmiger Anfragen an einen einzigen Endpunkt
- Zuerst Baselines festlegen — du musst die „normale" Latenz kennen, bevor du eine Verschlechterung erkennen kannst
- Last schrittweise steigern — plötzliche Spitzen sind DDoS-Simulationen, keine Lasttests
- Denkzeit einplanen — echte Nutzer pausieren zwischen Aktionen; ohne „sleep" erzeugen 50 VUs unrealistische Last
- Perzentile statt Durchschnittswerte prüfen — P95 und P99 zeigen die Erfahrung deiner am stärksten betroffenen Nutzer
- Server-Timing offenlegen —
Server-Timing-Header ermöglichen es dir, die Latenz aufzuschlüsseln, ohne den Test-Client zu ändern


