Por Lucas Yoris · Performate
HTTP/2 y HTTP/3 bajo carga: qué cambia en latencia, errores y costo TLS
Pruebas de carga HTTP/2 vs HTTP/3: multiplexación, QUIC, costos TLS y comparaciones honestas en k6—notas de evolución MDN y métricas HTTP de Grafana k6.
HTTP/3 recortó 20 ms en un slide de laboratorio—y luego las colas en producción empeoraron porque buffers UDP, cadenas de certificados y PoPs de CDN no se mantuvieron constantes. Los benchmarks de protocolo se vuelven arqueología operativa si no congelas política TLS, tamaños de payload y geografía.
HTTP/2 multiplexa muchas peticiones sobre una conexión TCP; HTTP/3 mueve el transporte a QUIC (basado en UDP) y suele mejorar el head-of-line blocking a costa de perfiles distintos de CPU y handshake (Evolution of HTTP, RFC 9114 visión general de QUIC para HTTP/3). Para equipos de API, la pregunta no es qué logo de RFC gana sino qué stack negocian tu edge y tus clientes bajo la política de certificados y cifrados de producción. Esta guía muestra cómo comparar H2 y H3 con honestidad en k6, qué modos de fallo difieren por protocolo y cuándo escalar colas vs tasa de error.
Combina experimentos con comportamiento de caché CDN y pruebas geo-distribuidas cuando los edges terminan stacks por región.
Por qué los cambios de protocolo mueven colas—no solo promedios
Bajo carga, las diferencias aparecen en:
- Setup de conexión — handshakes QUIC vs reutilización TCP+TLS; cold starts exageran latencia de primer byte.
- Paths con pérdida — HTTP/3 puede reducir head-of-line blocking en redes móviles; labs LAN quizá no muestran ganancia.
- Middleboxes — filtrado UDP causa resets que HTTP/2 no vio en el mismo path.
- CPU del servidor — cifrado y stacks QUIC en user-space desplazan costo de espera en kernel a quema de CPU.
Los promedios ocultan esos efectos. Compara p95/p99 con RPS idéntico (p95 vs p99) y etiqueta corridas proto:h2 vs proto:h3 (tags and groups).
k6 no reemplaza captura de paquetes. Usa pruebas de carga para cuantificar latencia visible al usuario y presupuestos de error bajo RPS sostenido; usa pcaps o logs de edge cuando necesites probar drops UDP en middleboxes o tormentas de renegociación TLS. El script anterior sirve para gates de regresión, no para certificación de conformidad RFC.
Cuando una «victoria» de protocolo es deriva de configuración
Cambiar publicidad ALPN sin actualizar límites udp del kernel o max_open_files en generadores produce regresiones falsas (fine-tuning OS). Documenta cambios de infra junto a toggles de protocolo para que los reportes sigan comparables.
Implementación práctica con k6: escenarios de protocolo etiquetados
k6 registra http_req_duration y tiempos relacionados—compara corridas solo si congelas cadena de certificados, política TLS, tamaños de payload y comportamiento del PoP CDN.
Script de ejemplo (ilustrativo—no listo para producción). Los endpoints deben exponer H2 y H3 en tu infra; hosts ficticios.
Qué demuestra este ejemplo:
- Escenarios paralelos golpeando URLs solo-H2 y capaces-H3 (o alias de host) al mismo RPS.
- Tags de protocolo para umbrales segmentados.
- Payload y headers idénticos para que compresión y auth coincidan.
- Umbrales de fallos y colas por protocolo, no una línea agregada.
import http from 'k6/http';
import { check, sleep } from 'k6';
const H2_BASE = __ENV.H2_BASE || 'https://h2.staging.example.com';
const H3_BASE = __ENV.H3_BASE || 'https://h3.staging.example.com';
const BODY = JSON.stringify({ query: 'latency-probe', size: 'medium' });
export const options = {
scenarios: {
proto_h2: {
executor: 'constant-arrival-rate',
rate: Number(__ENV.TARGET_RPS || 80),
timeUnit: '1s',
duration: '8m',
preAllocatedVUs: 30,
maxVUs: 120,
tags: { proto: 'h2' },
exec: 'hitH2',
},
proto_h3: {
executor: 'constant-arrival-rate',
rate: Number(__ENV.TARGET_RPS || 80),
timeUnit: '1s',
duration: '8m',
preAllocatedVUs: 30,
maxVUs: 120,
tags: { proto: 'h3' },
exec: 'hitH3',
},
},
thresholds: {
'http_req_duration{proto:h2}': ['p(95)<420', 'p(99)<750'],
'http_req_duration{proto:h3}': ['p(95)<400', 'p(99)<720'],
'http_req_failed{proto:h2}': ['rate<0.005'],
'http_req_failed{proto:h3}': ['rate<0.008'],
},
};
export function hitH2() {
const res = http.post(`${H2_BASE}/api/search`, BODY, {
headers: { 'Content-Type': 'application/json' },
tags: { proto: 'h2', route: 'search' },
});
check(res, { 'h2 2xx': (r) => r.status >= 200 && r.status < 300 });
sleep(0.1);
}
export function hitH3() {
const res = http.post(`${H3_BASE}/api/search`, BODY, {
headers: { 'Content-Type': 'application/json' },
tags: { proto: 'h3', route: 'search' },
});
check(res, { 'h3 2xx': (r) => r.status >= 200 && r.status < 300 });
sleep(0.1);
}
Patrones que funcionan
- Línea base del mix real de clientes (upgrades HTTP/1.1, H2 en todas partes o publicidad H3) antes de toggles sintéticos.
- Rampa modelos idénticos de RPS/VU; compara colas, no solo promedios.
- Registrar negociación desde ingress en la ventana de prueba; alinear tags con splits observados.
- Pares mesh on/off en Kubernetes cuando sidecars terminan TLS distinto (microservicios Kubernetes).
Antipatrones a evitar
- Benchmarkear tamaños de payload distintos «porque H3 es más nuevo».
- Ignorar tuning UDP del generador mientras culpas a la app por resets tempranos.
- Declarar victoria en staging LAN cuando paths móviles con pérdida mandan colas en prod.
Pro tip (comando de ejemplo):
k6 run http-proto-compare.js --summary-trend-stats="p(95),p(99)" -e TARGET_RPS=80
Qué demuestra este comando: tendencias de percentiles lado a lado para tags proto:h2 y proto:h3 en un resumen.
Marco de decisión: cuándo invertir en H3 bajo carga
| Señal | Suele ser protocolo cuando | Acción recomendada |
|---|---|---|
| Cola de latencia en alza, CPU estable | Buffering / pérdida QUIC en el path | Capturas; perfil de enlace con pérdida |
| Resets de conexión tempranos | Middleboxes que manejan mal UDP | Lab de firewall; política de fallback |
| Sesiones más cortas | Timeouts idle agresivos en path nuevo | Alinear keep-alive CDN/proxy con baseline H2 |
H3 gana en p99 solo móvil | Paths radio con pérdida | Pruebas geo + mix de dispositivos (geo-distribuidas) |
| H3 más CPU, colas similares | Costo cifrado/stack | Planificar headroom CPU |
Quédate en H2 si staging no muestra ganancia en colas y el costo ops de depurar UDP es alto.
Pilota H3 cuando logs de ingress muestran share material de H3 y colas móviles dominan incumplimientos de SLO.
Frena release cuando http_req_failed{proto:h3} supera presupuesto aunque p95 se vea mejor—los errores mandan sobre promedios.
Narrativa para stakeholders
Explica trade-offs con throughput vs latencia: HTTP/3 puede bajar colas en redes con pérdida mientras desplaza CPU del servidor—presupuesta ambos. Adjunta tags de protocolo a reportes (cómo leer reportes) para que liderazgo vea los mismos splits que ingeniería.
Checklist pre-release
- Documentar cadena TLS, política de cifrado y ALPN para corridas H2 vs H3.
- Congelar tamaño de payload, compresión y headers de auth entre escenarios de protocolo.
- Registrar tuning OS del generador (buffers
udp,max_open_files) en corridas H3. - Comparar
p95/p99yhttp_req_failedpor tagproto, no agregados globales. - Archivar stats de negociación de ingress de la ventana de prueba junto a exports k6.
Cómo Performate simplifica corridas A/B de protocolo
Alterna endpoints sin reescribir colecciones enteras cada release.
Ejemplo: comparar endpoints H2 y H3 desde una colección
- Importar requests de búsqueda/checkout una vez; duplicar hosts para alias
h2.yh3.. Problema resuelto: mismos bodies y headers entre protocolos. - Crear dos escenarios con la misma tasa de llegada y tags
proto:h2/proto:h3. Problema resuelto: RPS honesto sin forkear scripts. - Correr y abrir vista de comparación filtrada por tag de protocolo. Problema resuelto: colas visibles por stack en un export.
- Adjuntar notas de infra (PoP CDN, cambio de cert) en el pie del reporte para la reunión de adopción.
- Exportar k6 para regresión CI cuando el share de H3 cruce tu umbral de adopción.
- Compartir con SRE junto al runbook de depuración si los resets pican a mitad de prueba.
Cierre
Las comparaciones de protocolo son experimentos controlados, no slides con slogans. Congela TLS, payloads y geografía; etiqueta cada petición; juzga colas y errores antes que promedios.
Corre tu próximo par H2/H3 a RPS idéntico—y registra si se movió primero p99 o http_req_failed. Esa respuesta dice si el stack está listo, no solo más rápido en papel.
¿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.