Por Performate
Pruebas de carga WebSocket con k6: conexiones, heartbeats y backpressure
Pruebas de carga WebSocket con k6: conexiones concurrentes, heartbeats, backpressure—docs API WebSocket k6 más patrones de emparejamiento REST.
Tu dashboard REST muestra p99 verde en 120 ms, pero el chat en vivo se cae a los 2 000 usuarios conectados porque nadie stress-testeó sockets concurrentes—solo el POST de login que entrega el token. El reverse proxy tiene worker_connections para 1 024; el backend asume heartbeats cada 30 s; y el reconnect replay dispara un thundering herd que tumba Redis.
El protocolo WebSocket (RFC 6455) hace upgrade de HTTP a un canal bidireccional. La carga se mide en sockets concurrentes, mensajes/seg y churn de reconexión—no solo en http_req_duration de llamadas REST. En esta guía verás dimensiones de estrés, un escenario k6 con el módulo WebSocket experimental, y cómo emparejar fases HTTP + WS con realismo.
Por qué WebSocket cambia el perfil de carga—no solo el transporte
A nivel funcional, REST y WS comparten auth. Bajo concurrencia, ejercitan recursos distintos:
- Fan-out de conexiones — cada socket consume FD en proxy, LB y app; superar
worker_connectionsencuentra techos antes que CPU. - Heartbeats + idle — ping/pong desalineados o timeouts idle del proxy crean caídas silenciosas—not errores HTTP visibles.
- Burst tras reconnect — clientes replay suscripciones; servidores deben sobrevivir thundering herds post-deploy.
- Backpressure — publicar más rápido de lo que el consumer procesa llena buffers—not lo captura latencia REST.
Piensa en medir un call center solo contando emails mientras 5 000 líneas telefónicas simultáneas son el cuello real.
Cuando REST-only load tests pasan pero el realtime falla
La mayoría de productos intercalan REST y sockets—login HTTP, luego feed WS. Corre escenarios mixtos para que pools de conexión y tokens interactúen con realismo (plantilla script mínimo). Etiqueta feeds lógicos con tags de observabilidad. Si server→client basta sin bidireccional, compara con SSE y long-polling.
Implementación práctica con k6: conexión, subscribe y heartbeat
Modela conexión WS tras auth REST, suscripción a un feed, ping periódico y cierre limpio. Coordina socket storms en staging compartido (ética de pruebas).
Script de ejemplo (ilustrativo—no es una prueba lista para producción). Usa API experimental de WebSocket; adapta URLs, intervals y payloads.
Qué demuestra este ejemplo:
- Fase REST previa: obtiene token antes del upgrade WS—como clientes reales.
- Conexión + subscribe: envía frame de suscripción tras
open. - Heartbeat: ping cada 25 s alineado a keepalive de prod.
- Tags por feed:
feed:orderssegmenta métricas en summary y APM.
import http from 'k6/http';
import { check, sleep } from 'k6';
import ws from 'k6/experimental/websockets';
const BASE_HTTP = __ENV.API_BASE || 'https://staging.example.com';
const BASE_WS = __ENV.WS_BASE || 'wss://staging.example.com';
const FEED = 'orders';
export const options = {
scenarios: {
ws_connections: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 100 },
{ duration: '5m', target: 500 },
{ duration: '2m', target: 500 },
{ duration: '1m', target: 0 },
},
exec: 'wsSession',
tags: { protocol: 'websocket' },
},
},
thresholds: {
http_req_failed: ['rate<0.01'],
ws_connecting: ['p(95)<2000'],
ws_msgs_received: ['count>0'],
},
};
export function wsSession() {
const login = http.post(
`${BASE_HTTP}/v1/auth/token`,
JSON.stringify({ clientId: 'load-bot' }),
{ headers: { 'Content-Type': 'application/json' }, tags: { route: 'auth' } }
);
check(login, { 'auth ok': (r) => r.status === 200 });
const token = login.json('accessToken');
const url = `${BASE_WS}/v1/ws?token=${token}`;
const res = ws.connect(url, { tags: { feed: FEED } }, (socket) => {
socket.on('open', () => {
socket.send(JSON.stringify({ action: 'subscribe', feed: FEED }));
});
socket.on('message', (data) => {
check(data, { 'message received': (d) => d.length > 0 });
});
socket.setInterval(() => {
socket.ping();
}, 25000);
socket.setTimeout(() => {
socket.close();
}, 120000);
});
check(res, { 'ws connected': (r) => r && r.status === 101 });
sleep(1);
}
Patrones que funcionan
- Ramp gradual de conexiones — encuentra
worker_connectionsantes del cliff. - Keepalive = prod — replica intervalos ping/pong y proxy idle timeouts.
- Escenario mixto REST+WS — auth, REST setup, luego socket en la misma VU.
- Reconnect churn — escenario separado que cierra y reabre sockets en ráfaga post-ramp.
Anti-patrones a evitar
- Medir solo REST cuando el incidente es fan-out de sockets.
- Abrir 10 000 conexiones instantáneas en staging compartido sin ventana acordada.
- Ignorar FD limits en el generador de carga—el benchmark miente si k6 es el cuello.
Tip pro (comando de ejemplo):
k6 run ws-load.js -e API_BASE=https://staging.example.com -e WS_BASE=wss://staging.example.com
Qué demuestra este comando: variables separadas para HTTP y WS—patrón común cuando el upgrade host difiere del API REST.
Marco de decisión: qué dimensionar primero
| Situación | Acción recomendada |
|---|---|
| Producto nuevo realtime | Ramp de conexiones; encuentra techo de proxy/LB |
| Incidente post-deploy | Escenario reconnect burst + replay subscribe |
| Caídas silenciosas tras idle | Alinear heartbeat a prod; probar timeout proxy |
| Gaming / trading bursts | Mensajes/seg + backpressure, no solo conexiones |
| Solo server→client | Evaluar SSE/long-polling antes de WS |
Dimensiona conexiones primero si el síntoma es « too many open files » o L4 limits.
Dimensiona heartbeats si las caídas correlacionan con idle, no con CPU.
Dimensiona reconnect si deploys o network blips disparan thundering herds.
Observabilidad, documentación y siguientes pasos
Las pruebas WS solo sirven si documentas límites y coordinación. Antes de escalar sockets:
- Documenta
worker_connections, FD limits en generators y expectativas de ephemeral ports. - Coordina ventana con platform en staging compartido (ética).
- Correlaciona tags
feed:*en k6 con traces APM por canal lógico. - Registra intervalos ping/pong de prod en el README del escenario.
- Archiva summary con
ws_connectingy conteos de mensajes por release.
Cómo Performate simplifica journeys REST + WebSocket
Mantener auth REST en un script y WS en otro produce drift. Abajo hay un ejemplo concreto de flujo para feed de orders—adapta feeds y payloads a tu producto.
Ejemplo: login REST → subscribe WS → heartbeat en un workspace
- Importa requests REST (auth token) y define URL WS en variables de entorno. Problema resuelto: pre-step HTTP y socket en un journey visible para PM.
- Crea escenario ramping-vus 100→500 conexiones con stages graduales. Problema resuelto: fan-out realista sin script WS aislado.
- Configura payload de subscribe y tag
feed:ordersen el panel. Problema resuelto: segmentación alineada al ejemplo k6. - Define intervalo heartbeat 25 s en el flujo visual—match prod keepalive. Problema resuelto: caídas idle detectables antes de prod.
- Corre y compara métricas WS vs REST en reporte integrado—detecta si auth REST es el cuello previo al upgrade. Problema resuelto: un export para backend y platform.
- Exporta script k6 con módulo experimental WS para nightly (CI/CD).
Ese flujo conecta directo con el cta de este post: escenarios ejecutables sin días de código de integración REST+WS.
Cierre
El riesgo realtime es un problema de conexiones y churn, no solo de latencia REST. Stress-testea sockets concurrentes, heartbeats alineados a prod y bursts de reconnect—etiquetados por feed—antes del próximo launch que promete « live » en el hero.
Corre una rampa a 500 conexiones en staging esta semana—and anota en qué stage apareció el primer timeout de proxy o FD limit.
¿Listo para optimizar el rendimiento de tu API?
Usa Performate para convertir este playbook en escenarios k6 ejecutables, umbrales y reportes compartibles sin perder días en código de integración.