k6 Load Testing: Stress-Test Your APIs Before Launch

Master k6 load testing for Node.js APIs. Covers virtual user ramp-ups, thresholds, scenarios, HTTP/2, WebSocket testing, Grafana dashboards, and CI integration patterns.

E
ECOSIRE Research and Development Team
|19 mars 202610 min de lecture2.3k Mots|

Fait partie de notre série Performance & Scalability

Lire le guide complet

Test 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 de fs, pas de setTimeout)
  • 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_duration est 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 terminaisonp50p95p99Budget d'erreur
GET / (page d'accueil)120 ms400 ms800 ms0,1%
GET /api/contacts50 ms150 ms300 ms0,5%
POST /api/contacts80 ms200 ms400 ms0,5%
POST /auth/login200 ms500 ms1000 ms1%
POST /billing/checkout500 ms1500ms3000ms1%
GET /blog/[slug]100 ms300 ms600 ms0,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.

E

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.

Discutez sur WhatsApp