k6: testes de carga e performance em APIs

k6 é uma ferramenta de testes de carga open-source escrita em Go com scripts em JavaScript, próxima do que você já conhece como desenvolvedor Node.js. Com ela você descobre gargalos antes que seus usuários descubram: como a API se comporta com 100 usuários simultâneos? E com 1000? E com um pico súbito de tráfego?

Instalar k6 e primeiro teste

Instalação e script básico de teste de carga:

javascript
# Instalar k6:
# macOS:
brew install k6

# Ubuntu/Debian:
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 update && sudo apt install k6

# Docker:
docker run --rm -i grafana/k6 run - < script.js

# Primeiro script (tests/carga/api-basico.js):
import http from 'k6/http'
import { sleep, check } from 'k6'
import { Rate } from 'k6/metrics'

const erros = new Rate('erros')

export const options = {
  vus: 10,          // 10 virtual users simultâneos
  duration: '30s',  // por 30 segundos
  thresholds: {
    http_req_duration: ['p(95)<500'],  // 95% das req em < 500ms
    http_req_failed: ['rate<0.01'],    // menos de 1% de erros
    erros: ['rate<0.05'],
  },
}

export default function () {
  const res = http.get('https://api.seudominio.com.br/api/health')

  check(res, {
    'status 200': (r) => r.status === 200,
    'latência < 200ms': (r) => r.timings.duration < 200,
  })

  erros.add(res.status !== 200)
  sleep(1)  // pausar 1s entre iterações (simular comportamento humano)
}

Cenários de carga: ramp-up, spike e soak

Diferentes perfis de carga para testar diferentes hipóteses:

javascript
// tests/carga/cenarios.js:
import http from 'k6/http'
import { sleep, check } from 'k6'

export const options = {
  scenarios: {
    // Cenário 1: rampa gradual — simula crescimento de tráfego normal
    ramp_up: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '2m', target: 50 },   // subir para 50 VUs em 2min
        { duration: '5m', target: 50 },   // manter 50 VUs por 5min
        { duration: '2m', target: 0 },    // descer para 0
      ],
    },

    // Cenário 2: spike — testar resiliência a pico repentino
    spike: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '30s', target: 10 },   // carga normal
        { duration: '10s', target: 200 },  // spike súbito!
        { duration: '3m', target: 200 },   // manter o pico
        { duration: '30s', target: 10 },   // voltar ao normal
      ],
    },

    // Cenário 3: soak test — resistência a carga sustentada por horas
    soak: {
      executor: 'ramping-vus',
      stages: [
        { duration: '5m', target: 30 },    // aquecimento
        { duration: '2h', target: 30 },    // 2 horas de carga contínua
        { duration: '5m', target: 0 },     // resfriamento
      ],
    },
  },

  thresholds: {
    'http_req_duration{scenario:ramp_up}': ['p(99)<1000'],
    'http_req_duration{scenario:spike}': ['p(95)<3000'],   // mais tolerante no spike
    http_req_failed: ['rate<0.05'],
  },
}

export default function () {
  const res = http.post(
    'https://api.seudominio.com.br/api/auth/login',
    JSON.stringify({ email: 'teste@exemplo.com', senha: 'senha-teste' }),
    { headers: { 'Content-Type': 'application/json' } }
  )
  check(res, { 'login com sucesso': (r) => r.status === 200 })
  sleep(Math.random() * 3 + 1)  // pausa aleatória de 1-4s (comportamento humano)
}

Testar endpoints autenticados e fluxos completos

Scripts para testar fluxos que exigem autenticação JWT:

javascript
// tests/carga/fluxo-completo.js:
import http from 'k6/http'
import { check, group, sleep } from 'k6'
import { SharedArray } from 'k6/data'

// Dados de teste pré-carregados (evitar criar usuário a cada iteração):
const usuarios = new SharedArray('usuarios', function () {
  return JSON.parse(open('./dados/usuarios-teste.json'))
})

export const options = {
  vus: 20,
  duration: '5m',
  thresholds: {
    'http_req_duration{name:login}': ['p(95)<300'],
    'http_req_duration{name:listar-pedidos}': ['p(95)<500'],
    'http_req_duration{name:criar-pedido}': ['p(95)<1000'],
  },
}

export default function () {
  const usuario = usuarios[Math.floor(Math.random() * usuarios.length)]

  // ── Fase 1: Login ──────────────────────────────────────────────
  group('autenticação', () => {
    const loginRes = http.post(
      'https://api.seudominio.com.br/api/auth/login',
      JSON.stringify({ email: usuario.email, senha: usuario.senha }),
      { headers: { 'Content-Type': 'application/json' }, tags: { name: 'login' } }
    )

    check(loginRes, { 'login OK': (r) => r.status === 200 })
    const token = loginRes.json('accessToken') as string

    const headers = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }

    // ── Fase 2: Navegar pela aplicação ─────────────────────────────
    sleep(1)

    group('pedidos', () => {
      const listRes = http.get(
        'https://api.seudominio.com.br/api/v2/pedidos',
        { headers, tags: { name: 'listar-pedidos' } }
      )
      check(listRes, { 'lista OK': (r) => r.status === 200 })

      sleep(Math.random() * 2)

      const criarRes = http.post(
        'https://api.seudominio.com.br/api/v2/pedidos',
        JSON.stringify({ itens: [{ produto_id: usuario.produtoId, quantidade: 1 }] }),
        { headers, tags: { name: 'criar-pedido' } }
      )
      check(criarRes, { 'pedido criado': (r) => r.status === 201 })
    })
  })

  sleep(Math.random() * 3 + 2)
}

Exportar métricas do k6 para Prometheus

Ver métricas de carga em tempo real no Grafana durante o teste:

bash
# k6 pode exportar métricas para Prometheus via remote write:

# k6 com output para Prometheus Remote Write:
K6_PROMETHEUS_RW_SERVER_URL=http://localhost:9090/api/v1/write   k6 run --out experimental-prometheus-rw script.js

# Ou via InfluxDB (mais simples para uso local):
# docker run -p 8086:8086 influxdb:1.8
k6 run --out influxdb=http://localhost:8086/k6 script.js

# Dashboard Grafana para k6 + InfluxDB:
# Importar ID 2587 — k6 Load Testing Results (oficial Grafana)

# Configurar no docker-compose.yml:
services:
  influxdb:
    image: influxdb:1.8
    environment:
      INFLUXDB_DB: k6
    ports:
      - "8086:8086"

  grafana:
    environment:
      GF_DATASOURCES_DEFAULT_TYPE: influxdb
      GF_DATASOURCES_DEFAULT_URL: http://influxdb:8086
      GF_DATASOURCES_DEFAULT_DATABASE: k6

# Rodar k6 apontando para o InfluxDB do Docker:
docker run --rm -i --network host grafana/k6 run   --out influxdb=http://localhost:8086/k6 - < script.js

Integrar testes de carga no CI/CD

Rodar k6 automaticamente antes de cada deploy em produção:

yaml
# .github/workflows/carga.yml:
name: Testes de Carga

on:
  push:
    branches: [main]

jobs:
  k6:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Instalar 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 update && sudo apt install k6

      - name: Rodar teste de carga (staging)
        run: |
          k6 run \
            --env BASE_URL=https://staging.seudominio.com.br \
            --out json=resultados.json \
            tests/carga/api-basico.js

      - name: Upload resultados
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: k6-resultados
          path: resultados.json

Não quer configurar manualmente?

Implante o VPS para Node.js em menos de 5 minutos com a Runstack. Infraestrutura da OPEN Datacenter, com servidores em São Paulo.

Perguntas frequentes

k6: scripts em JavaScript, resultado binário (Go), excelente para devs Node.js, integração nativa com Grafana Cloud, open-source com versão cloud para testes distribuídos. Locust: scripts em Python, interface web em tempo real, bom para equipes com background Python. Artillery: também JavaScript/YAML, mais simples para scripts básicos, boa integração com AWS. Para times Node.js: k6 é a escolha mais natural, sintaxe familiar, ótima documentação, integração com ecossistema Prometheus/Grafana.

k6 é muito eficiente: uma máquina comum gera 10.000+ VUs sem problema. O gargalo geralmente é a rede e a capacidade de processamento de respostas HTTP, não os VUs em si. Para testes de até 500-1000 VUs: roda bem em laptop moderno ou VPS de 2 vCPUs. Para testes massivos (10k+ VUs): use k6 Cloud ou distribua o teste em múltiplas máquinas com k6 run --execution-segment.

Depende do que você quer detectar: memory leaks de Node.js geralmente aparecem em 30-60 minutos de carga sustentada. Vazamentos de conexões de banco: 1-4 horas. Problemas de disco (logs crescendo, arquivos temporários): 4-24 horas. Para CI/CD: um soak test de 30 minutos a cada deploy detecta a maioria dos memory leaks. Para releases críticas: rodar 2-4h em staging antes de promover para produção.

Para testes de carga: use um ambiente de staging com banco separado. Para testes em produção (limited scope): crie usuários de teste dedicados (usuario_teste_k6@seudominio.com), limite as ações (não criar pedidos reais), e limpe após o teste. Para dados seed: use k6 setup() e teardown() para criar dados antes do teste e limpar depois. Nunca aponte k6 para produção sem entender exatamente o que o script faz.

k6 retorna exit code 99 quando thresholds falham, o CI detecta como falha automaticamente. Na prática: investigar o relatório JSON gerado, identificar qual threshold falhou (p(95) > 500ms? error rate > 1%?), correlacionar com métricas do Prometheus no mesmo período, e decidir se é regressão de performance ou threshold mal calibrado para o ambiente de staging. Evite thresholds excessivamente rigorosos para staging se ele tiver hardware inferior ao de produção.