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 de março de 202610 min de leitura2.1k Palavras|

Parte da nossa série Performance & Scalability

Leia o guia completo

Teste de carga k6: teste de resistência de suas APIs antes do lançamento

Enviar um produto sem teste de carga é uma aposta. Você não sabe qual é o seu ponto de ruptura até que os usuários o encontrem para você – geralmente durante o lançamento de um produto, um momento viral ou um pico de vendas. k6 é a ferramenta moderna de teste de carga que permite escrever testes em JavaScript, executá-los a partir de CI e descobrir seu limite de desempenho antes dos usuários. É amigável ao desenvolvedor, eficiente em termos de recursos (k6 usa goroutines, não threads) e integra-se perfeitamente com Grafana e Prometheus para métricas em tempo real.

Este guia cobre o k6 desde o primeiro script até testes de carga complexos em vários cenários, métricas personalizadas, limites, integração de CI e padrões de teste seguros para produção para APIs Node.js/NestJS.

Principais conclusões

  • Os scripts k6 são JavaScript, mas são executados em um tempo de execução Go — sem APIs Node.js (sem require, sem fs, sem setTimeout)
  • Usuários Virtuais (VUs) são usuários simulados simultâneos; iterações são execuções de script individuais
  • Sempre defina limites antes de executar — limites com falha interrompem o teste e falham no CI
  • Usar cenários (vus constante, vus em rampa, taxa de chegada constante) para modelar padrões de tráfego reais
  • Nunca carregue a produção de teste sem coordenação com as operações — teste em relação à preparação ou a um clone de produção
  • A métrica http_req_duration é sua principal métrica de SLA — p95 e p99 são mais importantes que as médias
  • Limite de taxa para seus corredores k6 – o tráfego de teste de carga não deve esgotar seu orçamento de limite de taxa
  • Use k6 Cloud ou Grafana k6 para testes de carga distribuídos em regiões geográficas

Instalação e primeiro 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

Seu primeiro 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
}

Execute:

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

Teste de carga de API com autenticação

// 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)}`);
}

Cenários VU de aceleração

Modele padrões de tráfego reais com vários estágios e cenários:

// 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'],
  },
};

Teste de rastreamento de blog/sitemap

// 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étricas e limites personalizados

// 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);
}

Integração de saída Grafana

Transmita métricas k6 para o InfluxDB e visualize no Grafana durante o teste:

# 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

CI Integration

# .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/

Linhas de base de desempenho

Estabeleça linhas de base e alerte sobre regressões:

Ponto finalp50p95p99Erro no orçamento
GET / (página inicial)120ms400ms800ms0,1%
CÓDIGO050ms150ms300ms0,5%
CÓDIGO080ms200ms400ms0,5%
CÓDIGO0200ms500ms1000ms1%
CÓDIGO0500ms1500ms3000ms1%
CÓDIGO0100ms300ms600ms0,1%

Perguntas frequentes

Qual ​​é a diferença entre VUs e solicitações por segundo?

VUs (Usuários Virtuais) são usuários simulados simultâneos. Cada VU executa sua função padrão em um loop. As solicitações por segundo dependem da rapidez com que cada iteração é executada. Se cada iteração da VU levar 2 segundos (incluindo o tempo de suspensão) e você tiver 100 VUs, obterá aproximadamente 50 RPS. Use o executor constant-arrival-rate quando precisar direcionar um RPS específico, independentemente da duração da iteração.

Devo carregar o teste em produção ou teste?

Prefira sempre a encenação. Os testes de carga de produção correm o risco de impactar usuários reais, esgotar os limites de taxa, criar pedidos reais a partir de dados de teste e acionar eventos reais de webhook. Se você precisar testar a produção, faça-o durante janelas de baixo tráfego, use tokens de pagamento de teste e tenha um plano de reversão. O k6 Cloud oferece suporte à geração de carga distribuída geograficamente para que você possa testar o desempenho da borda do CDN sem tocar na origem.

Como lidar com a expiração do token de autenticação durante testes longos?

Para testes curtos (menos de 15 minutos), obtenha um token em setup() e passe-o para todas as VUs via data. Para testes longos (mais de 15 minutos), implemente a atualização do token no script de teste: verifique a idade do token na função padrão e atualize quando ele expirar. Armazene o token em uma variável JavaScript local para cada VU.

Qual é a diferença entre stages e scenarios?

stages é uma abreviação para um único cenário ramping-vus – bom para padrões simples de aceleração/manutenção/desaceleração. scenarios oferece controle total: vários padrões de tráfego simultâneos, diferentes executores por cenário, limites por cenário e marcação de cenário em métricas. Use scenarios para testes realistas de vários padrões (linha de base + pico + absorção simultaneamente).

Quantas VUs um único processo k6 pode manipular?

Um único processo k6 em hardware moderno pode sustentar de 5.000 a 10.000 VUs e gerar de 50.000 a 100.000 RPS, dependendo da complexidade do script e do tamanho da resposta. Para cargas mais altas, use k6 cloud ou execute várias instâncias k6 atrás de um distribuidor de carga. Cada VU é extremamente leve (uma goroutine) — k6 usa muito menos recursos que JMeter ou Gatling para contagens de VU equivalentes.


Próximas etapas

O teste de carga revela o verdadeiro perfil de desempenho do seu aplicativo antes que os usuários o descubram sob estresse. Os padrões neste guia (cenários de rampa, limites personalizados, integração de CI e painéis Grafana) fornecem a infraestrutura para encontrar e corrigir gargalos de desempenho continuamente.

ECOSIRE cria APIs NestJS com desempenho validado com testes de carga k6 cobrindo página inicial, endpoints de API, fluxos de checkout e rastreamentos completos de mapas de sites. Explore nossos serviços de engenharia de back-end para saber como construímos para obter desempenho desde o primeiro dia.

E

Escrito por

ECOSIRE Research and Development Team

Construindo produtos digitais de nível empresarial na ECOSIRE. Compartilhando insights sobre integrações Odoo, automação de e-commerce e soluções de negócios com IA.

Converse no WhatsApp