Realizar Load Testing en APIs
Cómo simular tráfico realista, medir tiempos de respuesta e identificar cuellos de botella usando k6 y JMeter para APIs y servicios web.
Visión general
El load testing mide cómo se comporta un sistema bajo un volumen específico de usuarios o requests concurrentes. A diferencia de los tests funcionales que verifican correctitud, los load tests revelan límites de rendimiento: ¿en qué punto el tiempo de respuesta degrada de 50ms a 2 segundos? ¿En qué carga los errores saltan de 0.1% a 10%? ¿Cuándo se agota el pool de conexiones a la base de datos?
Herramientas modernas como k6 y JMeter permiten definir escenarios en código o configuración, ejecutarlos desde la línea de comandos o pipelines de CI, y exportar métricas detalladas. La solucion a continuacion cubre cómo diseñar load tests realistas, interpretar los resultados e iterar sobre mejoras de rendimiento.
Cuándo usarlo
Usa esta receta cuando:
- Te preparas para un lanzamiento de producto, campaña de marketing o pico de tráfico estacional. Consulta Connection Pooling para manejar conexiones concurrentes a base de datos.
- Migras infraestructura y necesitas validar que la nueva plataforma maneja carga equivalente
- Estableces baselines de rendimiento y Objetivos de Nivel de Servicio (SLOs). Consulta Caching Strategies para reducir carga en servicios backend.
- Investigas timeouts o errores intermitentes que solo aparecen bajo carga concurrente. Consulta Rate Limiting para proteger APIs bajo tráfico intenso.
- Comparas rendimiento antes y después de un cambio mayor de código o infraestructura
Solución
k6 (JavaScript/Go)
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 100 }, // ramp up a 100 usuarios
{ duration: '5m', target: 100 }, // sostener carga
{ duration: '2m', target: 200 }, // ramp up a 200 usuarios
{ duration: '5m', target: 200 }, // sostener carga mayor
{ duration: '2m', target: 0 }, // ramp down
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% de requests bajo 500ms
http_req_failed: ['rate<0.01'], // tasa de error bajo 1%
},
};
export default function () {
const res = http.get('https://api.example.com/users');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
sleep(1);
}
JMeter (XML/GUI)
<ThreadGroup testname="API Load Test" guiclass="ThreadGroupGui">
<stringProp name="ThreadGroup.num_threads">100</stringProp>
<stringProp name="ThreadGroup.ramp_time">60</stringProp>
<stringProp name="ThreadGroup.duration">300</stringProp>
<elementProp name="HTTPsampler" elementType="HTTPSamplerProxy">
<stringProp name="HTTPSampler.domain">api.example.com</stringProp>
<stringProp name="HTTPSampler.path">/users</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
</elementProp>
</ThreadGroup>
Analizando Resultados (k6)
http_req_duration..............: avg=234ms min=45ms med=198ms max=1.2s p(90)=412ms p(95)=567ms
http_req_failed................: 0.23%
data_received..................: 12 MB
iterations.....................: 12000
Explicación
- Virtual Users (VUs): Usuarios concurrentes simulados que hacen requests. 100 VUs no significan 100 requests por segundo — depende del think time (
sleep) y la latencia de respuesta. - Ramp-up: Incrementar VUs gradualmente previene una avalancha repentina que distorsionaría los resultados. Un ramp de 2 minutos a 100 VUs es más realista que 100 VUs instantáneos.
- Thresholds: Criterios de pass/fail definidos antes del test. Si la latencia p(95) excede 500ms, k6 sale con código no cero, fallando el build de CI.
- Escenarios: Diferentes comportamientos de usuario modelados simultáneamente. Un test realista de e-commerce podría tener 80% de usuarios navegando, 15% agregando al carrito, y 5% haciendo checkout.
Variantes
| Herramienta | Scripting | Mejor para | Infraestructura |
|---|---|---|---|
| k6 | JavaScript/Go | Developer-friendly, CI-native | Self-hosted o cloud |
| JMeter | XML/GUI | Protocolos complejos, equipos enterprise | Self-hosted |
| Artillery | YAML/JS | Configuración rápida, equipos Node | Self-hosted o cloud |
| Locust | Python | Ecosistemas Python, lógica custom | Self-hosted |
Lo que funciona
- Testea contra un entorno similar a producción: testear localhost con CPU single-core da resultados sin sentido.
- Calienta el sistema primero: caches, pools de conexiones y compilación JIT necesitan tiempo para estabilizarse.
- Monitorea métricas server-side durante el test: correlaciona picos de latencia de k6 con logs de queries lentas de base de datos, uso de CPU y presión de memoria.
- Usa distribuciones de datos realistas: Las distribuciones uniformes raramente coinciden con la realidad.
- Testea endpoints idempotentes: los writes no idempotentes (pagos, deducciones de inventario) requieren manejo especial para evitar corromper datos de producción.
Errores comunes
- Testear desde una sola máquina: tu generador de carga puede convertirse en el cuello de botella.
- Ignorar latencia de red: testear una API en el mismo datacenter subestima la latencia real del mundo.
- Correr tests cortos: un test de 30 segundos te dice casi nada. Tests significativos corren por al menos 10 minutos para capturar ciclos de garbage collection y warmup de cache.
- No validar respuestas: una respuesta de 200ms que devuelve una página de error no es un éxito. Siempre asserta status codes y contenido del body.
Referencia Rápida
- Comando principal: ejecuta la solución base del artículo y verifica el resultado esperado.
- Validación: confirma que los tests pasan y que las métricas clave no se degradaron.
- Rollback: si algo falla, revierte el cambio y consulta la sección de Troubleshooting.
Lectura Adicional
- Documentación oficial: consulta la referencia actualizada del framework o herramienta utilizada.
- Guías relacionadas: explora las guías de testing y api-testing para profundizar.
- Patrones complementarios: revisa los patrones de diseño aplicables a tu stack tecnológico.
- Postmortems públicos: estudia incidentes reales de equipos que enfrentaron problemas similares en producción.
Notas de Producción
- Despliega gradualmente usando canary o blue-green para detectar regresiones temprano.
- Configura alertas para errores, latencia p99 y tasa de fallos antes de habilitar en producción.
- Documenta el rollback en el runbook; prueba el procedimiento en staging al menos una vez por trimestre.
- Revisa logs estructurados con correlation IDs para trazar requests end-to-end en incidentes.
Puntos Clave
- Aplica realizar load testing en apis cuando necesites una solución práctica para tu caso de uso.
- Monitorea el rendimiento después de implementar; mide latencia, errores y uso de recursos antes y después.
- Revisa la sección de Troubleshooting ante errores comunes; la mayoría tienen causa raíz documentada con solución.
- Mantén dependencias actualizadas y ejecuta tests en CI para prevenir regresiones en producción.
Ver También
- Integration Testing — testing de interacciones entre servicios
- Rate Limiting — protección de APIs bajo tráfico intenso
- Connection Pooling — manejo de conexiones concurrentes a base de datos
- Caching Strategies — reducción de carga backend
- API Documentation OpenAPI — documentación de contratos de API
Última actualización: 2026-07-09
Troubleshooting
- Flaky tests: isolate shared state, time, and randomness. Make tests independent and deterministic; quarantine persistently flaky tests.
- High coverage but bugs in production: coverage does not guarantee correctness. Add mutation testing, property-based tests, or contract tests.
- Slow test suite: parallelize, mock slow dependencies, and avoid end-to-end tests for logic that can be unit tested.
- Tests pass locally but fail in CI: check environment differences, timezone, locale, and dependency versions. Pin tool versions.
- Debugging a failing integration test: Reset state before each test.
Errores Comunes en Producción
- Copiar el ejemplo sin adaptarlo a volúmenes y modos de fallo reales.
- Saltar tests de carga e inyección de errores antes del primer despliegue productivo.
- Codificar valores fijos que deberían ser configurables por entorno.
- Olvidar agregar logging y monitoreo en cada paso.
- Desplegar sin plan de rollback ni estrategia de backup probada.
- Asumir que el ejemplo mínimo escalará sin agregar caché o procesamiento por lotes.
- No documentar la versión y configuración usadas en producción.
- Dejar la receta sin cambios cuando evolucionan las dependencias o la escala.
Preguntas frecuentes
¿Cuántos virtual users necesito para simular tráfico real?
Modela usuarios concurrentes, no usuarios totales. Si tienes 10,000 usuarios diarios pero solo 500 activos en cualquier momento, testea con 500 VUs (más un margen de seguridad de 20-50%). Calcula usuarios concurrentes desde analytics: concurrent_users = (daily_users * avg_session_duration_seconds) / 86400. Para un sitio con 100,000 usuarios diarios y sesiones promedio de 5 minutos: (100000 * 300) / 86400 = 347 usuarios concurrentes. Testea con 500 VUs para contar picos. Para API testing, calcula RPS desde tráfico de hora pico: si manejas 360,000 requests en la hora pico, eso son 100 RPS. Usa escenarios de k6 con diferentes arrival rates: scenarios: { browsing: { executor: 'ramping-arrival-rate', startRate: 10, timeUnit: '1s', stages: [{ target: 100, duration: '2m' }] } }.
¿Cuál es la diferencia entre load testing y stress testing?
El load testing valida comportamiento en niveles de tráfico esperados. El stress testing empuja más allá de los niveles esperados para encontrar el punto de ruptura y observar comportamiento de recuperación. El soak testing corre a carga normal por períodos extendidos (horas) para detectar memory leaks y agotamiento de recursos. El spike testing incrementa carga repentinamente para verificar que el sistema maneja bursts súbitos. El breakpoint testing incrementa carga incrementalmente hasta que el sistema falla, identificando el umbral exacto de falla. Cada tipo de test sirve un propósito diferente: load tests validan compliance de SLO, stress tests revelan modos de falla, soak tests detectan issues de larga duración, spike tests verifican autoscaling. En k6, implementa cada uno: // Stress test\nexport const options = { stages: [{ duration: '10m', target: 1000 }] };\n// Soak test\nexport const options = { stages: [{ duration: '4h', target: 200 }] };\n// Spike test\nexport const options = { stages: [{ duration: '10s', target: 500 }, { duration: '1m', target: 500 }, { duration: '10s', target: 0 }] }.
¿Puedo ejecutar load tests en pipelines de CI/CD?
Sí. k6 y Artillery están diseñados para esto. Corre smoke tests nocturnos (carga pequeña) y tests de regresión pre-release (carga completa) en tu pipeline. En GitHub Actions: name: Load Test\non: pull_request\njobs:\n k6:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: grafana/k6-action@v0.3.1\n with:\n filename: tests/load/test.js\n flags: --quiet --thresholds. Usa thresholds de k6 para fallar el build: thresholds: { http_req_duration: ['p(95)<500'], http_req_failed: ['rate<0.01'] }. Para CI cost-effective, corre smoke tests con 10-20 VUs por 1 minuto en cada PR, y load tests completos con 500+ VUs solo en branches de release. Usa k6 cloud para tests distribuidos: k6 cloud test.js --vus 500 --duration 10m. Cachea dependencias de test en CI: npm ci para scripts de k6, mvn dependency:resolve para JMeter.
¿Debería testear producción directamente?
Solo con extrema precaución. Usa transacciones sintéticas, endpoints read-only y horas de menor tráfico. Prefiere staging para tests destructivos o write-heavy. Para testing en producción, usa shadow traffic: espeja requests reales a un endpoint de test sin afectar usuarios. En k6, usa el flag --out para exportar métricas sin impactar producción: k6 run --out json=results.json test.js. Para tests read-only en producción: export default function () { const res = http.get('https://api.example.com/health'); check(res, { 'status is 200': (r) => r.status === 200 }); }. Usa feature flags para aislar tráfico de test: rutea requests de test a un pool de backend separado. Monitorea métricas de producción durante el test: si la tasa de error excede 0.5%, aborta el test inmediatamente. Usa el flag --abort-on-error de k6: k6 run --abort-on-error test.js. Para sistemas de pagos, usa endpoints sandbox que simulan el procesador de pagos sin cobros reales.
¿Cómo correlaciono resultados de load test con métricas server-side?
Ejecuta load tests mientras monitoreas métricas server-side para identificar cuellos de botella. Usa Prometheus y Grafana: # docker-compose.yml\nservices:\n prometheus:\n image: prom/prometheus\n grafana:\n image: grafana/grafana. En k6, exporta métricas a Prometheus: k6 run --out experimental-prometheus=http://prometheus:9090 test.js. Correlaciona latencia de k6 con métricas de base de datos: query Prometheus para pg_stat_database_tup_returned durante la ventana de test. Usa distributed tracing con Jaeger: instrumenta la API con OpenTelemetry, luego traza requests lentos específicos encontrados en resultados de k6. En k6, añade tags custom para tracing: const res = http.get('https://api.example.com/users', { tags: { test_run: 'nightly-2025-01-15' } });. Monitorea métricas JVM para aplicaciones Java: jcmd <pid> GC.heap_info durante el test. Trackea uso de connection pool: SELECT count(*) FROM pg_stat_activity WHERE state = 'active' durante el test. Usa APM tools como Datadog o New Relic para overlay de métricas de k6 con métricas de server en un solo dashboard.
¿Cómo manejo autenticación en load tests?
Maneja autenticación logueando una vez por iteración de VU y reutilizando tokens. Para Bearer tokens: import http from 'k6/http';\nconst token = __ENV.API_TOKEN;\nexport default function () {\n const res = http.get('https://api.example.com/users', {\n headers: { Authorization: Bearer ${token} }\n });\n};. Para flows de OAuth2 login: export default function () {\n const loginRes = http.post('https://api.example.com/oauth/token', {\n client_id: 'test_client',\n client_secret: 'test_secret',\n grant_type: 'client_credentials'\n });\n const token = loginRes.json('access_token');\n http.get('https://api.example.com/users', {\n headers: { Authorization: Bearer ${token} }\n });\n}. Para performance, cachea tokens entre iteraciones: let cachedToken = null;\nexport function setup() {\n const res = http.post('https://api.example.com/oauth/token', { ... });\n return { token: res.json('access_token') };\n}\nexport default function (data) {\n http.get('https://api.example.com/users', {\n headers: { Authorization: Bearer ${data.token} }\n });\n}. Usa las funciones setup() y teardown() de k6 para login/logout. Para JWT con refresh, maneja expiración de token: if (Date.now() > tokenExpiry) { refreshToken(); }.
¿Cómo testeo conexiones WebSocket con k6?
k6 soporta testing de WebSocket para aplicaciones real-time. Crea una conexión WebSocket: import ws from 'k6/ws';\nexport default function () {\n const url = 'wss://api.example.com/ws';\n ws.connect(url, {}, (socket) => {\n socket.on('open', () => {\n socket.send(JSON.stringify({ type: 'subscribe', channel: 'updates' }));\n });\n socket.on('message', (data) => {\n check(data, { 'has payload': (d) => JSON.parse(d).payload !== undefined });\n });\n socket.setInterval(() => {\n socket.send(JSON.stringify({ type: 'ping' }));\n }, 30000);\n socket.setTimeout(() => {\n socket.close();\n }, 60000);\n });\n}. Testea estabilidad de conexión bajo carga: export const options = {\n vus: 100,\n duration: '5m',\n thresholds: {\n ws_sessions_opened: ['count>0'],\n ws_msgs_received: ['rate>10'],\n ws_sessions_closed: ['rate<0.1']\n }\n};. Mide latencia de mensajes: socket.on('message', (data) => {\n const msg = JSON.parse(data);\n if (msg.timestamp) {\n const latency = Date.now() - msg.timestamp;\n console.log(WS latency: ${latency}ms);\n }\n});. Testea lógica de reconexión: cierra conexiones random y verifica que el cliente reconecta dentro de 5 segundos.
¿Cómo parametrizo load tests con test data?
Usa archivos CSV de datos o genera test data dinámicamente. Con k6, carga datos CSV: import papaparse from 'https://jslib.k6.io/papaparse/5.1.1/index.js';\nconst users = papaparse.parse(open('./users.csv'), { header: true }).data;\nexport default function () {\n const user = users[Math.floor(Math.random() * users.length)];\n http.post('https://api.example.com/login', {\n email: user.email,\n password: user.password\n });\n}. Genera data random: import { randomString, randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';\nexport default function () {\n const email = user${randomIntBetween(1, 10000)}@test.com;\n http.post('https://api.example.com/users', { email, name: randomString(8) });\n}. Usa execution contexts de k6 para data única por VU: export default function () {\n const vuId = __VU;\n const iterId = __ITER;\n const email = user-${vuId}-${iterId}@test.com;\n http.post('https://api.example.com/users', { email });\n}. Para JMeter, usa CSV Data Set Config: <CSVDataSet filename="users.csv" variableNames="email,password" delimiter="," recycle=false/>. Para datasets grandes, usa base de datos: const db = sql.open('postgres', 'host=localhost dbname=testdb');\nexport default function () {\n const user = db.query('SELECT email, password FROM test_users ORDER BY RANDOM() LIMIT 1')[0];\n http.post('https://api.example.com/login', { email: user.email, password: user.password });\n}.
¿Cómo mido percentile latencies correctamente?
Percentile latencies (p50, p90, p95, p99) proveen mejor insight que promedios. Un p99 de 2 segundos significa que 1% de usuarios experimentan delays de 2+ segundos. En k6, configura thresholds: thresholds: {\n http_req_duration: ['p(50)<200', 'p(90)<500', 'p(95)<800', 'p(99)<2000']\n}. Interpreta percentiles: p50 (mediana) muestra experiencia típica de usuario, p90 muestra el extremo lento, p99 muestra tail latency. No uses promedios para latencia: unos pocos outliers de 10 segundos pueden hacer un promedio de 200ms engañoso. Usa histogramas para visualización: k6 run --out json=results.json test.js && jq '.metrics.http_req_duration.values' results.json. Para mediciones precisas, corre tests suficientemente largo: 10+ minutos para percentiles estables. Descarta los primeros 2 minutos (warmup) del análisis. Usa --summary-export de k6 para output machine-readable: k6 run --summary-export=summary.json test.js. Compara percentiles across test runs para trackear regresiones: almacena resultados en una time series database y alerta cuando p95 incrementa más de 20%.
¿Cómo testeo rate limiting de API bajo carga?
Verifica comportamiento de rate limiting enviando requests por encima del umbral de rate limit. En k6: export const options = {\n scenarios: {\n burst: {\n executor: 'constant-arrival-rate',\n rate: 200,\n timeUnit: '1s',\n duration: '1m',\n preAllocatedVUs: 300\n }\n },\n thresholds: {\n http_req_failed: ['rate<0.15']\n }\n};\nexport default function () {\n const res = http.get('https://api.example.com/api');\n check(res, {\n 'status is 200 or 429': (r) => r.status === 200 || r.status === 429,\n 'has rate limit headers': (r) => r.headers['X-RateLimit-Limit'] !== undefined\n });\n}. Verifica que la response 429 incluya header Retry-After: check(res, {\n '429 has Retry-After': (r) => r.status !== 429 || r.headers['Retry-After'] !== undefined\n});. Testea recuperación de rate limit: después de hitting el límite, espera y verifica que los requests funcionan de nuevo: if (res.status === 429) {\n const retryAfter = parseInt(res.headers['Retry-After']);\n sleep(retryAfter + 1);\n const retryRes = http.get('https://api.example.com/api');\n check(retryRes, { 'recovered': (r) => r.status === 200 });\n}. Testea rate limits por usuario: usa diferentes API keys por VU. Testea comportamiento sliding window vs fixed window: envía requests en el boundary de la ventana.
Recursos Relacionados
Escribir Tests de Integración
Cómo testear múltiples componentes trabajando juntos usando bases de datos reales, clientes HTTP y colas de mensajes en Python, JavaScript y Java.
RecipeLimitacion de tasa (Rate Limiting)
Cómo implementar rate limiting en APIs usando token bucket, sliding window y fixed window en Python, JavaScript y Java.
RecipeConfigurar connection pooling para bases de datos y HTTP
Configura connection pooling para PostgreSQL, MySQL, Redis y clientes HTTP en Python, JavaScript y Java. Reduce latencia y evita agotamiento de conexiones.
RecipeTesting de Carga de APIs con k6 y Aserciones Basadas en
Como escribir y ejecutar tests de carga con k6 para medir rendimiento de APIs, validar SLOs e identificar cuellos de botella antes del despliegue a produccion
RecipeEscribir Unit Tests con Mocks y Stubs
Cómo aislar código bajo test usando objetos mock, stubs y spies para reemplazar dependencias externas como bases de datos, APIs y sistemas de archivos.