Saltar al contenido

Pruebas de carga para tu API: herramientas, patrones y errores comunes

Guía práctica de pruebas de carga en APIs: elección de herramientas, tráfico realista, líneas base, cuellos de botella y errores comunes.

5 min de lectura
Panel de resultados de pruebas de carga que muestra la tasa de solicitudes, los percentiles de latencia y las gráficas de tasa de errores

Las pruebas de carga responden a una pregunta simple: ¿qué sucede cuando el tráfico real llega a tu sistema? No se trata de la capacidad teórica, sino del comportamiento real bajo carga. La mayoría de los problemas de rendimiento son invisibles con poco tráfico y catastróficos con mucho tráfico. Las pruebas de carga los detectan antes de que lo hagan tus usuarios.

El reto no es ejecutar una prueba de carga. El reto es diseñar una prueba que produzca resultados significativos. Bombardear un endpoint con 10.000 solicitudes por segundo te dice algo, pero probablemente no lo que necesitas saber.

Elegir la herramienta adecuada

Distintas herramientas se ajustan a distintas necesidades. La mejor herramienta es la que tu equipo realmente va a usar.

jsjavascript
// 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
}

La diferencia clave respecto a otras herramientas: los scripts de k6 son JavaScript real. Puedes usar variables, bucles, condicionales y estado compartido. Las pruebas se leen como código de aplicación, no como configuración XML.

Patrones de tráfico realistas

El mayor error en las pruebas de carga: enviar tráfico uniforme a un único endpoint.

jsjavascript
// ❌ 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.
jsjavascript
// ✅ 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)];
}

La distribución del tráfico (60% navegación, 25% búsqueda, 10% checkout, 5% cuenta) debe coincidir con tus analíticas de producción. Si solo el 1% del tráfico real va al checkout, probar el checkout con el 50% de la carga da resultados sin sentido.

Establecer líneas base

Antes de hacer pruebas de carga, establece líneas base con solicitudes individuales. Necesitas saber cómo se ve lo "normal" antes de poder identificar lo "degradado".

jsjavascript
// 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

Identificar cuellos de botella

Los resultados de las pruebas de carga apuntan a cuellos de botella. Los patrones son predecibles.

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

Expón los encabezados Server-Timing de tu API en entornos de staging. Te permiten descomponer la latencia total en tiempo de base de datos, búsquedas en caché y procesamiento de la aplicación, sin necesidad de instrumentar el cliente de la prueba de carga.

Errores comunes

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

Conclusiones clave

  1. Modela tráfico realista — haz coincidir la distribución de tu tráfico de producción, no solicitudes uniformes a un solo endpoint
  2. Establece líneas base primero — necesitas conocer la latencia "normal" antes de poder detectar una degradación
  3. Aumenta la carga de forma gradual — los picos repentinos son simulaciones de DDoS, no pruebas de carga
  4. Incluye tiempo de espera entre acciones — los usuarios reales hacen pausas entre acciones; sin "sleep", 50 VUs generan una carga poco realista
  5. Revisa percentiles, no promedios — P95 y P99 revelan la experiencia de tus usuarios más afectados
  6. Expón el tiempo del servidor — los encabezados Server-Timing te permiten descomponer la latencia sin modificar el cliente de la prueba
Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX