Fait partie de notre série Performance & Scalability
Lire le guide completTest de charge k6 : testez sous contrainte vos API avant le lancement
Envoyer un produit sans test de charge est un pari. Vous ne connaissez pas votre point de rupture jusqu'à ce que les utilisateurs le trouvent pour vous – généralement lors du lancement d'un produit, d'un moment viral ou d'un pic de ventes. k6 est l'outil de test de charge moderne qui vous permet d'écrire des tests en JavaScript, de les exécuter à partir de CI et de découvrir votre plafond de performances avant les utilisateurs. Il est convivial pour les développeurs, économe en ressources (k6 utilise des goroutines, pas des threads) et s'intègre parfaitement à Grafana et Prometheus pour des métriques en temps réel.
Ce guide couvre k6 depuis le premier script jusqu'aux tests de charge multi-scénarios complexes, aux métriques personnalisées, aux seuils, à l'intégration CI et aux modèles de test sécurisés en production pour les API Node.js/NestJS.
Points clés à retenir
- Les scripts k6 sont JavaScript mais s'exécutent dans un runtime Go — pas d'API Node.js (pas de
require, pas defs, pas desetTimeout)- Les utilisateurs virtuels (VU) sont des utilisateurs simulés simultanés ; les itérations sont des exécutions de script individuelles - Toujours définir des seuils avant l'exécution : les seuils d'échec arrêtent le test et font échouer le CI.
- Utiliser des scénarios (vus constant, ramping-vus, taux d'arrivée constant) pour modéliser les modèles de trafic réels
- Ne chargez jamais la production de test sans coordination avec les opérations - testez par rapport à une préparation ou à un clone de production
- La métrique
http_req_durationest votre principale métrique SLA – p95 et p99 comptent plus que les moyennes.- Limitez le débit de vos coureurs K6 - le trafic de test de charge ne devrait pas épuiser votre budget limite de débit
- Utilisez k6 Cloud ou Grafana k6 pour les tests de charge distribués dans les régions géographiques
Installation et premier script
# macOS
brew install k6
# Ubuntu/Debian
sudo gpg -k && sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update && sudo apt-get install k6
# Windows (via Chocolatey)
choco install k6
Votre premier script k6 :
// k6/homepage-load.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';
// Custom metric: error rate
const errorRate = new Rate('error_rate');
export const options = {
// Ramp up to 50 VUs over 1 minute, hold 2 minutes, ramp down
stages: [
{ duration: '1m', target: 50 },
{ duration: '2m', target: 50 },
{ duration: '1m', target: 0 },
],
thresholds: {
http_req_duration: ['p(95)<500', 'p(99)<1000'], // 95th pct < 500ms
http_req_failed: ['rate<0.01'], // Less than 1% errors
error_rate: ['rate<0.01'],
},
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
export default function () {
const res = http.get(`${BASE_URL}/`);
const ok = check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
'body contains ECOSIRE': (r) => r.body.includes('ECOSIRE'),
});
errorRate.add(!ok);
sleep(1); // 1 second think time between iterations
}
Exécutez-le :
k6 run k6/homepage-load.js
k6 run --env BASE_URL=https://staging.ecosire.com k6/homepage-load.js
k6 run --vus 100 --duration 30s k6/homepage-load.js # Quick smoke test
Test de charge API avec authentification
// k6/api-load.js
import http from 'k6/http';
import { check, group, sleep } from 'k6';
import { Counter, Trend } from 'k6/metrics';
const apiCalls = new Counter('api_calls');
const authDuration = new Trend('auth_duration');
export const options = {
scenarios: {
// Constant arrival rate: 100 requests/second regardless of VU count
constant_load: {
executor: 'constant-arrival-rate',
rate: 100,
timeUnit: '1s',
duration: '3m',
preAllocatedVUs: 50,
maxVUs: 200,
},
},
thresholds: {
http_req_duration: ['p(95)<300', 'p(99)<500'],
'http_req_duration{type:api}': ['p(95)<200'],
http_req_failed: ['rate<0.005'], // 0.5% error budget
},
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3001';
// setup() runs once before VUs start
export function setup() {
// Authenticate and return tokens for VUs to use
const loginRes = http.post(`${BASE_URL}/auth/login`, JSON.stringify({
email: __ENV.TEST_EMAIL,
password: __ENV.TEST_PASSWORD,
}), {
headers: { 'Content-Type': 'application/json' },
});
check(loginRes, { 'login successful': (r) => r.status === 200 });
const { accessToken } = loginRes.json();
return { accessToken };
}
export default function (data) {
const { accessToken } = data;
const headers = {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
};
group('Contacts API', () => {
// List contacts
const listRes = http.get(`${BASE_URL}/contacts?page=1&limit=20`, {
headers,
tags: { type: 'api', endpoint: 'contacts-list' },
});
check(listRes, {
'list contacts: 200': (r) => r.status === 200,
'list contacts: has items': (r) => r.json('data') !== null,
});
apiCalls.add(1);
sleep(0.5);
// Create a contact
const createRes = http.post(`${BASE_URL}/contacts`, JSON.stringify({
name: `Load Test User ${Date.now()}`,
email: `load-test-${Date.now()}@example.com`,
}), {
headers,
tags: { type: 'api', endpoint: 'contacts-create' },
});
check(createRes, {
'create contact: 201': (r) => r.status === 201,
});
apiCalls.add(1);
});
sleep(1);
}
// teardown() runs once after all VUs finish
export function teardown(data) {
// Clean up test data if needed
console.log(`Test complete. Data: ${JSON.stringify(data)}`);
}
Scénarios de rampe VU
Modélisez des modèles de trafic réels avec plusieurs étapes et scénarios :
// k6/stress-test.js
export const options = {
scenarios: {
// Scenario 1: Normal traffic baseline
normal_traffic: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 20 },
{ duration: '5m', target: 20 },
],
gracefulRampDown: '30s',
},
// Scenario 2: Sudden spike (marketing campaign goes live)
traffic_spike: {
executor: 'ramping-vus',
startVUs: 0,
startTime: '7m', // Starts after baseline is stable
stages: [
{ duration: '30s', target: 200 }, // Rapid spike
{ duration: '2m', target: 200 }, // Hold spike
{ duration: '30s', target: 20 }, // Fall back
],
},
// Scenario 3: Soak test for memory leaks
soak_test: {
executor: 'constant-vus',
vus: 10,
duration: '30m', // Run for 30 minutes
startTime: '10m',
},
},
thresholds: {
'http_req_duration{scenario:normal_traffic}': ['p(95)<300'],
'http_req_duration{scenario:traffic_spike}': ['p(95)<800'],
http_req_failed: ['rate<0.02'],
},
};
Test d'exploration du blog/du plan du site
// k6/sitemap-crawl.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { SharedArray } from 'k6/data';
// Load URLs from sitemap (pre-fetched and saved as JSON)
const urls = new SharedArray('sitemap urls', function () {
// In real usage, fetch sitemap XML and parse URLs beforehand
// SharedArray is loaded once and shared across all VUs (memory efficient)
return JSON.parse(open('./sitemap-urls.json'));
});
export const options = {
vus: 20,
duration: '5m',
thresholds: {
http_req_duration: ['p(95)<2000'], // Public pages can be slower
http_req_failed: ['rate<0.01'],
},
};
export default function () {
// Each VU picks a random URL from the sitemap
const url = urls[Math.floor(Math.random() * urls.length)];
const res = http.get(url, {
headers: {
// Identify as load tester in logs
'User-Agent': 'k6-load-tester/1.0',
},
timeout: '10s',
});
check(res, {
'status is 200': (r) => r.status === 200,
'no error page': (r) => !r.body.includes('Error'),
'has canonical tag': (r) => r.body.includes('rel="canonical"'),
'response time < 2000ms': (r) => r.timings.duration < 2000,
});
sleep(Math.random() * 2 + 1); // Random 1-3 second think time
}
Métriques et seuils personnalisés
// k6/checkout-load.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Counter, Rate, Trend, Gauge } from 'k6/metrics';
// Custom metrics for business-specific tracking
const checkoutSuccesses = new Counter('checkout_successes');
const checkoutFailures = new Counter('checkout_failures');
const checkoutDuration = new Trend('checkout_duration');
const activeCheckouts = new Gauge('active_checkouts');
export const options = {
stages: [
{ duration: '1m', target: 10 },
{ duration: '3m', target: 30 },
{ duration: '1m', target: 0 },
],
thresholds: {
checkout_duration: ['p(95)<3000'], // Checkout < 3s at p95
checkout_failures: ['count<10'], // Max 10 absolute failures
http_req_duration: ['p(99)<2000'],
},
};
export default function (data) {
activeCheckouts.add(1);
const start = Date.now();
// Step 1: Add to cart
const cartRes = http.post('/api/cart/add', JSON.stringify({
productId: 'prod_odoo_customization',
quantity: 1,
}), { headers: { 'Content-Type': 'application/json' } });
if (!check(cartRes, { 'add to cart: 200': (r) => r.status === 200 })) {
checkoutFailures.add(1);
activeCheckouts.add(-1);
return;
}
sleep(2);
// Step 2: Create checkout session
const checkoutRes = http.post('/api/billing/checkout', JSON.stringify({
cartId: cartRes.json('cartId'),
}), { headers: { 'Content-Type': 'application/json' } });
const success = check(checkoutRes, {
'checkout session created': (r) => r.status === 200,
'has session URL': (r) => r.json('url') !== null,
});
if (success) {
checkoutSuccesses.add(1);
checkoutDuration.add(Date.now() - start);
} else {
checkoutFailures.add(1);
}
activeCheckouts.add(-1);
sleep(1);
}
Intégration de la sortie Grafana
Diffusez les métriques k6 vers InfluxDB et visualisez-les dans Grafana pendant le test :
# Run with InfluxDB output
k6 run --out influxdb=http://localhost:8086/k6 k6/api-load.js
# Or use the k6 Grafana integration (k6 v0.45+)
k6 run --out experimental-prometheus-rw k6/api-load.js
# docker-compose.monitoring.yml
services:
influxdb:
image: influxdb:2.7
ports: ["8086:8086"]
environment:
INFLUXDB_DB: k6
INFLUXDB_ADMIN_USER: admin
INFLUXDB_ADMIN_PASSWORD: password
grafana:
image: grafana/grafana:latest
ports: ["3030:3000"]
environment:
GF_AUTH_ANONYMOUS_ENABLED: "true"
volumes:
- ./grafana/dashboards:/etc/grafana/provisioning/dashboards
- ./grafana/datasources:/etc/grafana/provisioning/datasources
Intégration CI
# .github/workflows/ci.yml (load test section)
load-tests:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' # Only on main branch
needs: [deploy-staging]
steps:
- uses: actions/checkout@v4
- name: Install k6
run: |
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg \
--keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" \
| sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update && sudo apt-get install k6
- name: Run smoke test (30 VUs, 60s)
run: |
k6 run --vus 30 --duration 60s \
--env BASE_URL=${{ secrets.STAGING_URL }} \
--env TEST_EMAIL=${{ secrets.TEST_EMAIL }} \
--env TEST_PASSWORD=${{ secrets.TEST_PASSWORD }} \
apps/api/k6/api-load.js
- name: Upload k6 results
uses: actions/upload-artifact@v4
if: always()
with:
name: k6-results
path: k6-results/
Références de performances
Établir des références et alerter sur les régressions :
| Point de terminaison | p50 | p95 | p99 | Budget d'erreur |
|---|---|---|---|---|
GET / (page d'accueil) | 120 ms | 400 ms | 800 ms | 0,1% |
GET /api/contacts | 50 ms | 150 ms | 300 ms | 0,5% |
POST /api/contacts | 80 ms | 200 ms | 400 ms | 0,5% |
POST /auth/login | 200 ms | 500 ms | 1000 ms | 1% |
POST /billing/checkout | 500 ms | 1500ms | 3000ms | 1% |
GET /blog/[slug] | 100 ms | 300 ms | 600 ms | 0,1% |
Questions fréquemment posées
Quelle est la différence entre les VU et les requêtes par seconde ?
Les VU (Virtual Users) sont des utilisateurs simulés simultanés. Chaque VU exécute votre fonction par défaut en boucle. Le nombre de requêtes par seconde dépend de la vitesse d'exécution de chaque itération. Si chaque itération de VU prend 2 secondes (temps de veille compris) et que vous disposez de 100 VU, vous obtenez environ 50 RPS. Utilisez l'exécuteur constant-arrival-rate lorsque vous devez cibler un RPS spécifique, quelle que soit la durée de l'itération.
Dois-je charger des tests par rapport à la production ou à la préparation ?
Préférez toujours la mise en scène. Les tests de charge de production risquent d'avoir un impact sur les utilisateurs réels, d'épuiser les limites de débit, de créer de véritables commandes à partir des données de test et de déclencher de véritables événements de webhook. Si vous devez tester la production, faites-le pendant les fenêtres à faible trafic, utilisez des jetons de paiement de test et disposez d'un plan de restauration. k6 Cloud prend en charge la génération de charge géo-distribuée afin que vous puissiez tester les performances de votre CDN Edge sans toucher à l'origine.
Comment gérer l'expiration du jeton d'authentification lors de tests longs ?
Pour les tests courts (moins de 15 minutes), obtenez un jeton en setup() et transmettez-le à tous les VU via data. Pour les tests longs (plus de 15 minutes), implémentez l'actualisation du jeton dans le script de test : vérifiez l'âge du jeton dans la fonction par défaut et actualisez-le lorsqu'il approche de son expiration. Stockez le jeton dans une variable JavaScript locale à chaque VU.
Quelle est la différence entre stages et scenarios ?
stages est un raccourci pour un seul scénario ramping-vus — idéal pour les modèles simples de montée/maintien/décélération. scenarios vous donne un contrôle total : plusieurs modèles de trafic simultanés, différents exécuteurs par scénario, seuils par scénario et marquage des scénarios dans les métriques. Utilisez scenarios pour des tests multi-modèles réalistes (ligne de base + pic + trempage simultanément).
Combien de VU un seul processus k6 peut-il gérer ?
Un seul processus k6 sur du matériel moderne peut prendre en charge 5 000 à 10 000 VU et générer 50 000 à 100 000 RPS, en fonction de la complexité du script et de la taille de la réponse. Pour des charges plus élevées, utilisez k6 cloud ou exécutez plusieurs instances k6 derrière un répartiteur de charge. Chaque VU est extrêmement léger (une goroutine) — k6 utilise beaucoup moins de ressources que JMeter ou Gatling pour un nombre de VU équivalent.
Prochaines étapes
Les tests de charge révèlent le véritable profil de performances de votre application avant que les utilisateurs ne le découvrent sous pression. Les modèles présentés dans ce guide (scénarios de montée en puissance, seuils personnalisés, intégration CI et tableaux de bord Grafana) vous fournissent l'infrastructure nécessaire pour rechercher et résoudre en permanence les goulots d'étranglement des performances.
ECOSIRE crée des API NestJS aux performances validées avec des tests de charge k6 couvrant la page d'accueil, les points de terminaison de l'API, les flux de paiement et les analyses complètes du plan du site. Découvrez nos services d'ingénierie backend pour découvrir comment nous construisons des performances dès le premier jour.
Rédigé par
ECOSIRE Research and Development Team
Création de produits numériques de niveau entreprise chez ECOSIRE. Partage d'analyses sur les intégrations Odoo, l'automatisation e-commerce et les solutions d'entreprise propulsées par l'IA.
Articles connexes
API Rate Limiting: Patterns and Best Practices
Master API rate limiting with token bucket, sliding window, and fixed counter patterns. Protect your backend with NestJS throttler, Redis, and real-world configuration examples.
GoHighLevel API and Webhooks: Custom Integrations
Complete developer guide to GoHighLevel API and webhooks. Build custom integrations, sync data with external systems, and extend GHL capabilities with REST API and webhook automation.
NestJS 11 Enterprise API Patterns
Master NestJS 11 enterprise patterns: guards, interceptors, pipes, multi-tenancy, and production-ready API design for scalable backend systems.
Plus de Performance & Scalability
Nginx Production Configuration: SSL, Caching, and Security
Nginx production configuration guide: SSL termination, HTTP/2, caching headers, security headers, rate limiting, reverse proxy setup, and Cloudflare integration patterns.
Odoo Performance Tuning: PostgreSQL and Server Optimization
Expert guide to Odoo 19 performance tuning. Covers PostgreSQL configuration, indexing, query optimization, Nginx caching, and server sizing for enterprise deployments.
Odoo vs Acumatica: Cloud ERP for Growing Businesses
Odoo vs Acumatica compared for 2026: unique pricing models, scalability, manufacturing depth, and which cloud ERP fits your growth trajectory.
Testing and Monitoring AI Agents in Production
A complete guide to testing and monitoring AI agents in production environments. Covers evaluation frameworks, observability, drift detection, and incident response for OpenClaw deployments.
Compliance Monitoring Agents with OpenClaw
Deploy OpenClaw AI agents for continuous compliance monitoring. Automate regulatory checks, policy enforcement, audit trail generation, and compliance reporting.
Optimizing AI Agent Costs: Token Usage and Caching
Practical strategies for reducing AI agent operational costs through token optimization, caching, model routing, and usage monitoring. Real savings from production OpenClaw deployments.