{"schemaVersion":1,"id":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance","url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance","title":"TCP vs QUIC vs HTTP/3: Kernel Bypass, Zero-Copy y Optimización de Latencia","description":"Benchmarks reales comparando TCP, QUIC y HTTP/3 con io_uring, DPDK y zero-copy networking. Algoritmos de control de congestión diseccionados con mediciones de latencia de producción.","language":"es","datePublished":"2025-12-07","dateModified":null,"contentFormat":"text/markdown","content":"Networking\n\n# TCP vs QUIC vs HTTP/3: Kernel Bypass, Zero-Copy y Optimización de Latencia\n\n2025-12-078 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/tcp-quic-http3-kernel-bypass-performance>)\n\n## El setup de testing\n\nAntes de examinar los números, vale la pena ser transparente con el hardware, demasiados posts de \"benchmarks\" se ejecutan en una instancia EC2 arbitraria con vecinos ruidosos.\n\n- **Server**: Bare metal, AMD EPYC 9454 (48C/96T), 256GB DDR5-4800\n- **NIC**: Mellanox ConnectX-7 (100GbE), dual-port\n- **Cliente**: Mismo hardware, conexión directa con cable DAC (sin switch)\n- **OS**: Ubuntu 24.04 LTS, kernel 6.8.0\n- **DPDK**: 23.11 LTS\n- **QUIC**: quiche 0.21 (la implementación de Cloudflare) y msquic 2.3\n- **io\\_uring**: liburing 2.6\n\nCada test corrió por 60 segundos, repetido 10 veces, tomando la mediana. ¿Frequency scaling? Deshabilitado. ¿Turbo boost? Deshabilitado. ¿IRQ affinity? Pinneado a cores específicos. ¿Hyperthreading? Apagado. Sin atajos.\n\n## TCP: la vieja guardia todavía muerde\n\nTCP plano sirve como la baseline contra la que todos comparan, y su rendimiento con kernels modernos se subestima con frecuencia.\n\n` send() `/` recv() ` estándar sobre TCP con config default del kernel:\n\n```c\n// TCP bloqueante clásico, nada fancy\nint sock = socket(AF_INET, SOCK_STREAM, 0);\nsetsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));\nconnect(sock, (struct sockaddr*)&addr, sizeof(addr));\n\n// Mensajes de 64KB, 1M iteraciones\nfor (int i = 0; i < 1000000; i++) {\n send(sock, buf, 65536, 0);\n}\n```\n\n**Resultados**: 14.2 Gbps throughput, 11.3us latencia media, 18.7us p99.\n\nRazonable, pero queda rendimiento significativo sin aprovechar. El problema es que cada llamada a ` send() ` es un syscall, cambio de contexto al kernel, copia del buffer, cambio de contexto de vuelta.\n\n### TCP + io\\_uring: envíos zero-copy\n\n[io\\_uring](<https://github.com/axboe/liburing>) con ` IORING_OP_SEND_ZC ` elimina la copia del buffer:\n\n```c\nstruct io_uring ring;\nio_uring_queue_init(256, &ring, 0);\n\nstruct io_uring_sqe *sqe = io_uring_get_sqe(&ring);\nio_uring_prep_send_zc(sqe, sock, buf, 65536, 0, 0);\nsqe->user_data = 1;\nio_uring_submit(&ring);\n\n// Esperar completion Y notificación de que el buffer se puede reusar\nstruct io_uring_cqe *cqe;\nio_uring_wait_cqe(&ring, &cqe);\nif (cqe->flags & IORING_CQE_F_NOTIF) {\n // El buffer se puede reusar ahora\n}\nio_uring_cqe_seen(&ring, cqe);\n```\n\n**Resultados**: 31.7 Gbps throughput, 5.8us latencia media, 9.2us p99.\n\nEso es 2.2x mejora en throughput y ~50% reducción de latencia. La clave: no es solo evitar la copia, el batching de submissions en el ring significa menos syscalls en total. Un solo ` io_uring_submit() ` puede empujar docenas de operaciones.\n\n### TCP + DPDK: kernel bypass total\n\nCuando se necesita absolutamente cada último nanosegundo:\n\n```c\n// Construcción de paquete DPDK, control total del stack\nstruct rte_mbuf *pkt = rte_pktmbuf_alloc(mbuf_pool);\nstruct rte_ether_hdr *eth = rte_pktmbuf_mtod(pkt, struct rte_ether_hdr*);\n// ... construir headers ethernet, IP, TCP manualmente ...\n\n// Acceso directo a la NIC, sin kernel de por medio\nuint16_t nb_tx = rte_eth_tx_burst(port_id, queue_id, &pkt, 1);\n```\n\n**Resultados**: 82.4 Gbps throughput, 1.1us latencia media, 1.8us p99.\n\nEste es el resultado de eliminar el kernel completamente. El costo es significativo: se debe reimplementar TCP en espacio de usuario. En la práctica, este enfoque se reserva para workloads específicos de trading de alta frecuencia, y la experiencia de debugging requiere considerable expertise.\n\n## QUIC: no es simplemente \"TCP 2.0\"\n\nQUIC recibe mucho hype como \"el futuro del networking\" pero la realidad es más matizada. Corre en userspace, lo que significa que no aplican los trucos de kernel bypass, y obliga TLS 1.3, así que cada paquete lleva overhead de encriptación.\n\nEl test utilizó la implementación [quiche](<https://github.com/cloudflare/quiche>) de [Cloudflare](<https://www.cloudflare.com/>), extensamente probada en producción:\n\n```rust\n// Setup de conexión QUIC con quiche\nlet mut config = quiche::Config::new(quiche::PROTOCOL_VERSION)?;\nconfig.set_application_protos(b\"\\x02h3\")?;\nconfig.set_max_idle_timeout(30000);\nconfig.set_initial_max_data(10_000_000);\nconfig.set_initial_max_stream_data_bidi_local(1_000_000);\nconfig.enable_early_data();\n\nlet conn = quiche::connect(Some(\"server\"), &scid, local, peer, &mut config)?;\n\n// Enviar datos en un stream\nlet written = conn.stream_send(stream_id, &buf[..65536], false)?;\n```\n\n**Resultados**: 8.7 Gbps throughput, 22.4us latencia media, 38.1us p99.\n\n¿Más lento que TCP plano? Sí. Y eso es en una conexión directa con cero pérdida de paquetes. Sin embargo, QUIC no fue diseñado para ganar en este benchmark. Fue diseñado para la internet real, donde los paquetes se pierden, las conexiones migran entre redes, y el head-of-line blocking mata los streams multiplexados de HTTP/2.\n\n### Donde QUIC realmente gana\n\nSe configuró un perfil de tc netem simulando una conexión móvil típica: 50ms RTT, 1% pérdida de paquetes, 0.5% reordenamiento.\n\n| Protocolo | Throughput | Latencia p99 | Setup de Conexión |\n| --- | --- | --- | --- |\n| TCP + TLS 1.3 | 2.1 Gbps | 187ms | 3 RTT (SYN + TLS) |\n| QUIC | 3.8 Gbps | 112ms | 1 RTT (0-RTT resume) |\n| HTTP/3 | 3.6 Gbps | 98ms\\* | 0 RTT (resume) |\n\n\\*El p99 de HTTP/3 es menor porque los streams independientes evitan HOL blocking.\n\nEn redes con pérdida, el 0-RTT de QUIC y el multiplexing de streams independientes destruyen a TCP. El caso 0-RTT es particularmente brutal para TCP, para cuando TCP termina su three-way handshake y la negociación TLS, QUIC ya mandó la primera respuesta.\n\n## Control de congestión: el algoritmo importa más que el protocolo\n\nEste aspecto se suele pasar por alto. La elección del algoritmo de control de congestión tiene más impacto en el rendimiento real que TCP vs QUIC en la mayoría de los escenarios.\n\nSe testearon cuatro algoritmos en un link de 100ms RTT con pérdida variable:\n\n```bash\n# Cambiar control de congestión en Linux\nsysctl -w net.ipv4.tcp_congestion_control=bbr\n# o cubic, reno, bbr3 (requiere kernel parcheado)\n```\n\n### BBRv3 vs CUBIC vs Reno, 100ms RTT\n\nCon 0.1% de pérdida: BBRv3 empuja 42 Gbps contra 28 Gbps de CUBIC. El approach basado en modelo de BBR no retrocede tan agresivamente.\n\nCon 1% de pérdida: BBRv3 alcanza 18 Gbps, CUBIC cae a 8.3 Gbps. BBR es claramente superior aquí.\n\nCon 5% de pérdida: Acá viene la sorpresa, CUBIC se recupera a 3.1 Gbps mientras BBRv3 solo logra 2.4 Gbps. El bandwidth probing de BBR se confunde con tasas de pérdida altas sostenidas y oscila entre probing y draining.\n\nLa investigación sobre por qué BBRv3 underperforma con pérdida alta revela un hallazgo interesante. La respuesta está en cómo BBR estima el bottleneck bandwidth. Usa un filtro de máximo con ventana sobre delivery rates recientes, pero con 5% de pérdida, el stream de ACKs se vuelve tan ruidoso que el filtro max sigue tomando mediciones outlier, haciendo que la tasa de envío oscile salvajemente.\n\n```python\n# Estimación de bandwidth de BBR simplificada\nclass BBRBandwidthFilter:\n def __init__(self, window_size=10):\n self.window = deque(maxlen=window_size)\n\n def update(self, delivery_rate):\n self.window.append(delivery_rate)\n # Con pérdida alta, estas muestras están por todos lados\n return max(self.window) # Este max es el problema\n```\n\n## HTTP/3: el panorama completo\n\nHTTP/3 es QUIC con semánticas HTTP encima. Las características de performance heredan de QUIC, pero hay una capa adicional que vale la pena discutir: la compresión de headers QPACK.\n\nQPACK es la respuesta de HTTP/3 a HPACK, pero está diseñado para la entrega fuera de orden de QUIC. Esto significa que no puede usar la máquina de estados secuencial simple que usa HPACK. En cambio, tiene streams de encoder y decoder que operan independientemente:\n\n```\n// Actualización de tabla dinámica QPACK\nEncoder Stream: INSERT name=\":status\" value=\"200\"\n INSERT name=\"content-type\" value=\"application/json\"\n\n// El bloque de headers referencia entradas de la tabla dinámica\nRequest Stream: INDEXED ref=0 (\":status: 200\")\n INDEXED ref=1 (\"content-type: application/json\")\n```\n\nEn la práctica, QPACK logra 5-15% peor compresión que HPACK en tráfico web típico (porque no puede arriesgarse a bloquear), pero la eliminación del head-of-line blocking compensa de sobra.\n\n## El framework de decisión práctico\n\nDespués de todos estos benchmarks, aquí va un framework de decisión práctico:\n\n**Utilizar TCP plano + io\\_uring** cuando se controlan ambos endpoints, la red es confiable y se necesita throughput bruto. Aplicaciones típicas: replicación inter-datacenter, protocolos de cable de base de datos, data planes de service mesh.\n\n**Utilizar QUIC** cuando los clientes están en redes poco confiables (móvil, WiFi), se necesita migración de conexión, o multiplexing de streams independientes. Aplicaciones típicas: APIs client-facing, comunicación en tiempo real, edge de CDN.\n\n**Utilizar DPDK** únicamente cuando se necesita latencia sub-2us y se cuenta con un equipo capaz de mantener un stack de networking personalizado. Aplicaciones típicas: HFT, procesamiento de paquetes telco, appliances de DPI.\n\n**Evitar HTTP/3** salvo que exista un requerimiento claro. Si los clientes son navegadores, lo negociarán automáticamente via headers Alt-Svc. Si los clientes son servicios controlados, el overhead del framing HTTP probablemente no se justifica, utilizar QUIC directamente o permanecer con TCP.\n\n## Un punto adicional: GSO y GRO\n\nGeneric Segmentation Offload y Generic Receive Offload son componentes fundamentales del networking moderno que suelen pasar desapercibidos. Permiten entregar a la NIC un buffer de 64KB y que el hardware lo divida en segmentos del tamaño del MTU:\n\n```c\n// Habilitar GSO en un socket UDP (para QUIC)\nint gso_size = 1200; // Tamaño de paquete QUIC\nsetsockopt(fd, SOL_UDP, UDP_SEGMENT, &gso_size, sizeof(gso_size));\n\n// Ahora se pueden enviar 64KB en un solo syscall\n// El kernel/NIC lo divide en paquetes UDP de 1200 bytes\nsendmsg(fd, &msg, 0); // msg contiene buffer de 64KB\n```\n\nEsta sola optimización puede mejorar el throughput de QUIC 3.2x. Sin GSO, cada paquete QUIC requiere una llamada ` sendmsg() ` separada. Con GSO, se agrupan 50+ paquetes en un syscall. Así es como Cloudflare logra que QUIC rinda a escala.\n\nEl panorama del networking está cambiando rápido. io\\_uring suma funcionalidades de zero-copy en cada release del kernel, las implementaciones de QUIC maduran velozmente, y el kernel bypass se está volviendo más accesible. Pero los fundamentos no han cambiado: medir el workload real, en la red real, con el hardware real. Los benchmarks sintéticos son un punto de partida, no una respuesta.\n\n## Preguntas frecuentes\n\n### ¿QUIC es siempre más rápido que TCP?\n\nNo. En redes de baja latencia y poca pérdida, TCP con kernel bypass (io\\_uring o DPDK) suele igualar o superar a QUIC porque QUIC corre en userspace y tiene overhead de encriptación. QUIC brilla en conexiones con pérdida, alta latencia y escenarios de migración de conexión.\n\n### ¿Qué es kernel bypass y cuándo debería usarlo?\n\nKernel bypass permite que la aplicación envíe y reciba paquetes sin pasar por el stack de networking del SO. Tecnologías como DPDK e io\\_uring reducen cambios de contexto y copias de memoria. Se recomienda cuando se necesita latencia sub-milisegundo o se procesan millones de paquetes por segundo.\n\n### ¿Cómo funciona zero-copy networking con io\\_uring?\n\nLa operación IORING\\_OP\\_SEND\\_ZC de io\\_uring pasa referencias a buffers al kernel en vez de copiar datos. El kernel hace DMA directo desde el buffer de aplicación a la NIC. Se recibe un evento de completion cuando el buffer se puede reutilizar.\n\n## Tips\n\n1. **Tip:** Utilizar ` io_uring ` con ` IORING_OP_SEND_ZC ` para envíos zero-copy, reduce el overhead de syscalls aproximadamente un 40% en workloads TCP de alto throughput.\n2. **Tip:** El 0-RTT de QUIC reduce significativamente la latencia para clientes móviles, pero es vulnerable a replay attacks, siempre validar ` early_data ` en la capa de aplicación.\n3. **Tip:** Al realizar benchmarks con DPDK, fijar los threads a cores aislados con ` isolcpus ` y deshabilitar IRQ balancing, de lo contrario los números son ruido estadístico.\n4. **Tip:** BBRv3 no es una solución universal. En links con \\>2% de pérdida de paquetes, CUBIC sigue siendo superior. Es necesario perfilar las condiciones de red reales antes de cambiar.\n\n- tcp\n- quic\n- http3\n- io\\_uring\n- dpdk\n- zero-copy\n- kernel-bypass\n- networking\n- performance\n- congestion-control\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)","headings":[{"level":1,"text":"TCP vs QUIC vs HTTP/3: Kernel Bypass, Zero-Copy y Optimización de Latencia","url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#title"},{"level":2,"text":"El setup de testing","url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#el-setup-de-testing"},{"level":2,"text":"TCP: la vieja guardia todavía muerde","url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#tcp-la-vieja-guardia-todava-muerde"},{"level":3,"text":"TCP + io_uring: envíos zero-copy","url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#tcp-iouring-envos-zero-copy"},{"level":3,"text":"TCP + DPDK: kernel bypass total","url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#tcp-dpdk-kernel-bypass-total"},{"level":2,"text":"QUIC: no es simplemente \"TCP 2.0\"","url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#quic-no-es-simplemente-tcp-20"},{"level":3,"text":"Donde QUIC realmente gana","url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#donde-quic-realmente-gana"},{"level":2,"text":"Control de congestión: el algoritmo importa más que el protocolo","url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#control-de-congestin-el-algoritmo-importa-ms-que-el-protocolo"},{"level":3,"text":"BBRv3 vs CUBIC vs Reno, 100ms RTT","url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#bbrv3-vs-cubic-vs-reno-100ms-rtt"},{"level":2,"text":"HTTP/3: el panorama completo","url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#http3-el-panorama-completo"},{"level":2,"text":"El framework de decisión práctico","url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#el-framework-de-decisin-prctico"},{"level":2,"text":"Un punto adicional: GSO y GRO","url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#un-punto-adicional-gso-y-gro"},{"level":2,"text":"Preguntas frecuentes","url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#questions"},{"level":3,"text":"¿QUIC es siempre más rápido que TCP?","url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#question-1"},{"level":3,"text":"¿Qué es kernel bypass y cuándo debería usarlo?","url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#question-2"},{"level":3,"text":"¿Cómo funciona zero-copy networking con io_uring?","url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#article-tips"}],"sections":[{"heading":"TCP vs QUIC vs HTTP/3: Kernel Bypass, Zero-Copy y Optimización de Latencia","level":1,"url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance","content":"Networking"},{"heading":"TCP vs QUIC vs HTTP/3: Kernel Bypass, Zero-Copy y Optimización de Latencia","level":1,"url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#title","content":"2025-12-078 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/tcp-quic-http3-kernel-bypass-performance>)"},{"heading":"El setup de testing","level":2,"url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#el-setup-de-testing","content":"Antes de examinar los números, vale la pena ser transparente con el hardware, demasiados posts de \"benchmarks\" se ejecutan en una instancia EC2 arbitraria con vecinos ruidosos.\n\n- **Server**: Bare metal, AMD EPYC 9454 (48C/96T), 256GB DDR5-4800\n- **NIC**: Mellanox ConnectX-7 (100GbE), dual-port\n- **Cliente**: Mismo hardware, conexión directa con cable DAC (sin switch)\n- **OS**: Ubuntu 24.04 LTS, kernel 6.8.0\n- **DPDK**: 23.11 LTS\n- **QUIC**: quiche 0.21 (la implementación de Cloudflare) y msquic 2.3\n- **io\\_uring**: liburing 2.6\n\nCada test corrió por 60 segundos, repetido 10 veces, tomando la mediana. ¿Frequency scaling? Deshabilitado. ¿Turbo boost? Deshabilitado. ¿IRQ affinity? Pinneado a cores específicos. ¿Hyperthreading? Apagado. Sin atajos."},{"heading":"TCP: la vieja guardia todavía muerde","level":2,"url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#tcp-la-vieja-guardia-todava-muerde","content":"TCP plano sirve como la baseline contra la que todos comparan, y su rendimiento con kernels modernos se subestima con frecuencia.\n\n` send() `/` recv() ` estándar sobre TCP con config default del kernel:\n\n```c\n// TCP bloqueante clásico, nada fancy\nint sock = socket(AF_INET, SOCK_STREAM, 0);\nsetsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));\nconnect(sock, (struct sockaddr*)&addr, sizeof(addr));\n\n// Mensajes de 64KB, 1M iteraciones\nfor (int i = 0; i < 1000000; i++) {\n send(sock, buf, 65536, 0);\n}\n```\n\n**Resultados**: 14.2 Gbps throughput, 11.3us latencia media, 18.7us p99.\n\nRazonable, pero queda rendimiento significativo sin aprovechar. El problema es que cada llamada a ` send() ` es un syscall, cambio de contexto al kernel, copia del buffer, cambio de contexto de vuelta."},{"heading":"TCP + io_uring: envíos zero-copy","level":3,"url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#tcp-iouring-envos-zero-copy","content":"[io\\_uring](<https://github.com/axboe/liburing>) con ` IORING_OP_SEND_ZC ` elimina la copia del buffer:\n\n```c\nstruct io_uring ring;\nio_uring_queue_init(256, &ring, 0);\n\nstruct io_uring_sqe *sqe = io_uring_get_sqe(&ring);\nio_uring_prep_send_zc(sqe, sock, buf, 65536, 0, 0);\nsqe->user_data = 1;\nio_uring_submit(&ring);\n\n// Esperar completion Y notificación de que el buffer se puede reusar\nstruct io_uring_cqe *cqe;\nio_uring_wait_cqe(&ring, &cqe);\nif (cqe->flags & IORING_CQE_F_NOTIF) {\n // El buffer se puede reusar ahora\n}\nio_uring_cqe_seen(&ring, cqe);\n```\n\n**Resultados**: 31.7 Gbps throughput, 5.8us latencia media, 9.2us p99.\n\nEso es 2.2x mejora en throughput y ~50% reducción de latencia. La clave: no es solo evitar la copia, el batching de submissions en el ring significa menos syscalls en total. Un solo ` io_uring_submit() ` puede empujar docenas de operaciones."},{"heading":"TCP + DPDK: kernel bypass total","level":3,"url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#tcp-dpdk-kernel-bypass-total","content":"Cuando se necesita absolutamente cada último nanosegundo:\n\n```c\n// Construcción de paquete DPDK, control total del stack\nstruct rte_mbuf *pkt = rte_pktmbuf_alloc(mbuf_pool);\nstruct rte_ether_hdr *eth = rte_pktmbuf_mtod(pkt, struct rte_ether_hdr*);\n// ... construir headers ethernet, IP, TCP manualmente ...\n\n// Acceso directo a la NIC, sin kernel de por medio\nuint16_t nb_tx = rte_eth_tx_burst(port_id, queue_id, &pkt, 1);\n```\n\n**Resultados**: 82.4 Gbps throughput, 1.1us latencia media, 1.8us p99.\n\nEste es el resultado de eliminar el kernel completamente. El costo es significativo: se debe reimplementar TCP en espacio de usuario. En la práctica, este enfoque se reserva para workloads específicos de trading de alta frecuencia, y la experiencia de debugging requiere considerable expertise."},{"heading":"QUIC: no es simplemente \"TCP 2.0\"","level":2,"url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#quic-no-es-simplemente-tcp-20","content":"QUIC recibe mucho hype como \"el futuro del networking\" pero la realidad es más matizada. Corre en userspace, lo que significa que no aplican los trucos de kernel bypass, y obliga TLS 1.3, así que cada paquete lleva overhead de encriptación.\n\nEl test utilizó la implementación [quiche](<https://github.com/cloudflare/quiche>) de [Cloudflare](<https://www.cloudflare.com/>), extensamente probada en producción:\n\n```rust\n// Setup de conexión QUIC con quiche\nlet mut config = quiche::Config::new(quiche::PROTOCOL_VERSION)?;\nconfig.set_application_protos(b\"\\x02h3\")?;\nconfig.set_max_idle_timeout(30000);\nconfig.set_initial_max_data(10_000_000);\nconfig.set_initial_max_stream_data_bidi_local(1_000_000);\nconfig.enable_early_data();\n\nlet conn = quiche::connect(Some(\"server\"), &scid, local, peer, &mut config)?;\n\n// Enviar datos en un stream\nlet written = conn.stream_send(stream_id, &buf[..65536], false)?;\n```\n\n**Resultados**: 8.7 Gbps throughput, 22.4us latencia media, 38.1us p99.\n\n¿Más lento que TCP plano? Sí. Y eso es en una conexión directa con cero pérdida de paquetes. Sin embargo, QUIC no fue diseñado para ganar en este benchmark. Fue diseñado para la internet real, donde los paquetes se pierden, las conexiones migran entre redes, y el head-of-line blocking mata los streams multiplexados de HTTP/2."},{"heading":"Donde QUIC realmente gana","level":3,"url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#donde-quic-realmente-gana","content":"Se configuró un perfil de tc netem simulando una conexión móvil típica: 50ms RTT, 1% pérdida de paquetes, 0.5% reordenamiento.\n\n| Protocolo | Throughput | Latencia p99 | Setup de Conexión |\n| --- | --- | --- | --- |\n| TCP + TLS 1.3 | 2.1 Gbps | 187ms | 3 RTT (SYN + TLS) |\n| QUIC | 3.8 Gbps | 112ms | 1 RTT (0-RTT resume) |\n| HTTP/3 | 3.6 Gbps | 98ms\\* | 0 RTT (resume) |\n\n\\*El p99 de HTTP/3 es menor porque los streams independientes evitan HOL blocking.\n\nEn redes con pérdida, el 0-RTT de QUIC y el multiplexing de streams independientes destruyen a TCP. El caso 0-RTT es particularmente brutal para TCP, para cuando TCP termina su three-way handshake y la negociación TLS, QUIC ya mandó la primera respuesta."},{"heading":"Control de congestión: el algoritmo importa más que el protocolo","level":2,"url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#control-de-congestin-el-algoritmo-importa-ms-que-el-protocolo","content":"Este aspecto se suele pasar por alto. La elección del algoritmo de control de congestión tiene más impacto en el rendimiento real que TCP vs QUIC en la mayoría de los escenarios.\n\nSe testearon cuatro algoritmos en un link de 100ms RTT con pérdida variable:\n\n```bash\n# Cambiar control de congestión en Linux\nsysctl -w net.ipv4.tcp_congestion_control=bbr\n# o cubic, reno, bbr3 (requiere kernel parcheado)\n```"},{"heading":"BBRv3 vs CUBIC vs Reno, 100ms RTT","level":3,"url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#bbrv3-vs-cubic-vs-reno-100ms-rtt","content":"Con 0.1% de pérdida: BBRv3 empuja 42 Gbps contra 28 Gbps de CUBIC. El approach basado en modelo de BBR no retrocede tan agresivamente.\n\nCon 1% de pérdida: BBRv3 alcanza 18 Gbps, CUBIC cae a 8.3 Gbps. BBR es claramente superior aquí.\n\nCon 5% de pérdida: Acá viene la sorpresa, CUBIC se recupera a 3.1 Gbps mientras BBRv3 solo logra 2.4 Gbps. El bandwidth probing de BBR se confunde con tasas de pérdida altas sostenidas y oscila entre probing y draining.\n\nLa investigación sobre por qué BBRv3 underperforma con pérdida alta revela un hallazgo interesante. La respuesta está en cómo BBR estima el bottleneck bandwidth. Usa un filtro de máximo con ventana sobre delivery rates recientes, pero con 5% de pérdida, el stream de ACKs se vuelve tan ruidoso que el filtro max sigue tomando mediciones outlier, haciendo que la tasa de envío oscile salvajemente.\n\n```python\n# Estimación de bandwidth de BBR simplificada\nclass BBRBandwidthFilter:\n def __init__(self, window_size=10):\n self.window = deque(maxlen=window_size)\n\n def update(self, delivery_rate):\n self.window.append(delivery_rate)\n # Con pérdida alta, estas muestras están por todos lados\n return max(self.window) # Este max es el problema\n```"},{"heading":"HTTP/3: el panorama completo","level":2,"url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#http3-el-panorama-completo","content":"HTTP/3 es QUIC con semánticas HTTP encima. Las características de performance heredan de QUIC, pero hay una capa adicional que vale la pena discutir: la compresión de headers QPACK.\n\nQPACK es la respuesta de HTTP/3 a HPACK, pero está diseñado para la entrega fuera de orden de QUIC. Esto significa que no puede usar la máquina de estados secuencial simple que usa HPACK. En cambio, tiene streams de encoder y decoder que operan independientemente:\n\n```\n// Actualización de tabla dinámica QPACK\nEncoder Stream: INSERT name=\":status\" value=\"200\"\n INSERT name=\"content-type\" value=\"application/json\"\n\n// El bloque de headers referencia entradas de la tabla dinámica\nRequest Stream: INDEXED ref=0 (\":status: 200\")\n INDEXED ref=1 (\"content-type: application/json\")\n```\n\nEn la práctica, QPACK logra 5-15% peor compresión que HPACK en tráfico web típico (porque no puede arriesgarse a bloquear), pero la eliminación del head-of-line blocking compensa de sobra."},{"heading":"El framework de decisión práctico","level":2,"url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#el-framework-de-decisin-prctico","content":"Después de todos estos benchmarks, aquí va un framework de decisión práctico:\n\n**Utilizar TCP plano + io\\_uring** cuando se controlan ambos endpoints, la red es confiable y se necesita throughput bruto. Aplicaciones típicas: replicación inter-datacenter, protocolos de cable de base de datos, data planes de service mesh.\n\n**Utilizar QUIC** cuando los clientes están en redes poco confiables (móvil, WiFi), se necesita migración de conexión, o multiplexing de streams independientes. Aplicaciones típicas: APIs client-facing, comunicación en tiempo real, edge de CDN.\n\n**Utilizar DPDK** únicamente cuando se necesita latencia sub-2us y se cuenta con un equipo capaz de mantener un stack de networking personalizado. Aplicaciones típicas: HFT, procesamiento de paquetes telco, appliances de DPI.\n\n**Evitar HTTP/3** salvo que exista un requerimiento claro. Si los clientes son navegadores, lo negociarán automáticamente via headers Alt-Svc. Si los clientes son servicios controlados, el overhead del framing HTTP probablemente no se justifica, utilizar QUIC directamente o permanecer con TCP."},{"heading":"Un punto adicional: GSO y GRO","level":2,"url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#un-punto-adicional-gso-y-gro","content":"Generic Segmentation Offload y Generic Receive Offload son componentes fundamentales del networking moderno que suelen pasar desapercibidos. Permiten entregar a la NIC un buffer de 64KB y que el hardware lo divida en segmentos del tamaño del MTU:\n\n```c\n// Habilitar GSO en un socket UDP (para QUIC)\nint gso_size = 1200; // Tamaño de paquete QUIC\nsetsockopt(fd, SOL_UDP, UDP_SEGMENT, &gso_size, sizeof(gso_size));\n\n// Ahora se pueden enviar 64KB en un solo syscall\n// El kernel/NIC lo divide en paquetes UDP de 1200 bytes\nsendmsg(fd, &msg, 0); // msg contiene buffer de 64KB\n```\n\nEsta sola optimización puede mejorar el throughput de QUIC 3.2x. Sin GSO, cada paquete QUIC requiere una llamada ` sendmsg() ` separada. Con GSO, se agrupan 50+ paquetes en un syscall. Así es como Cloudflare logra que QUIC rinda a escala.\n\nEl panorama del networking está cambiando rápido. io\\_uring suma funcionalidades de zero-copy en cada release del kernel, las implementaciones de QUIC maduran velozmente, y el kernel bypass se está volviendo más accesible. Pero los fundamentos no han cambiado: medir el workload real, en la red real, con el hardware real. Los benchmarks sintéticos son un punto de partida, no una respuesta."},{"heading":"Preguntas frecuentes","level":2,"url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#questions","content":""},{"heading":"¿QUIC es siempre más rápido que TCP?","level":3,"url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#question-1","content":"No. En redes de baja latencia y poca pérdida, TCP con kernel bypass (io\\_uring o DPDK) suele igualar o superar a QUIC porque QUIC corre en userspace y tiene overhead de encriptación. QUIC brilla en conexiones con pérdida, alta latencia y escenarios de migración de conexión."},{"heading":"¿Qué es kernel bypass y cuándo debería usarlo?","level":3,"url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#question-2","content":"Kernel bypass permite que la aplicación envíe y reciba paquetes sin pasar por el stack de networking del SO. Tecnologías como DPDK e io\\_uring reducen cambios de contexto y copias de memoria. Se recomienda cuando se necesita latencia sub-milisegundo o se procesan millones de paquetes por segundo."},{"heading":"¿Cómo funciona zero-copy networking con io_uring?","level":3,"url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#question-3","content":"La operación IORING\\_OP\\_SEND\\_ZC de io\\_uring pasa referencias a buffers al kernel en vez de copiar datos. El kernel hace DMA directo desde el buffer de aplicación a la NIC. Se recibe un evento de completion cuando el buffer se puede reutilizar."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#article-tips","content":"1. **Tip:** Utilizar ` io_uring ` con ` IORING_OP_SEND_ZC ` para envíos zero-copy, reduce el overhead de syscalls aproximadamente un 40% en workloads TCP de alto throughput.\n2. **Tip:** El 0-RTT de QUIC reduce significativamente la latencia para clientes móviles, pero es vulnerable a replay attacks, siempre validar ` early_data ` en la capa de aplicación.\n3. **Tip:** Al realizar benchmarks con DPDK, fijar los threads a cores aislados con ` isolcpus ` y deshabilitar IRQ balancing, de lo contrario los números son ruido estadístico.\n4. **Tip:** BBRv3 no es una solución universal. En links con \\>2% de pérdida de paquetes, CUBIC sigue siendo superior. Es necesario perfilar las condiciones de red reales antes de cambiar.\n\n- tcp\n- quic\n- http3\n- io\\_uring\n- dpdk\n- zero-copy\n- kernel-bypass\n- networking\n- performance\n- congestion-control\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)"}],"links":[{"text":"English","url":"https://lucioduran.com/blog/tcp-quic-http3-kernel-bypass-performance"},{"text":"io_uring","url":"https://github.com/axboe/liburing"},{"text":"quiche","url":"https://github.com/cloudflare/quiche"},{"text":"Cloudflare","url":"https://www.cloudflare.com/"},{"text":"Volver al Blog","url":"https://lucioduran.com/es/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance"},"headline":"TCP vs QUIC vs HTTP/3: Kernel Bypass, Zero-Copy y Optimización de Latencia","description":"Benchmarks reales comparando TCP, QUIC y HTTP/3 con io_uring, DPDK y zero-copy networking. Algoritmos de control de congestión diseccionados con mediciones de latencia de producción.","datePublished":"2025-12-07","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://lucioduran.com/favicons/og-default.png","url":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance","inLanguage":"es","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"Networking","keywords":"tcp, quic, http3, io_uring, dpdk, zero-copy, kernel-bypass, networking, performance, congestion-control","wordCount":1485,"timeRequired":"PT8M","workTranslation":[{"@type":"BlogPosting","inLanguage":"en","url":"https://lucioduran.com/blog/tcp-quic-http3-kernel-bypass-performance"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Inicio","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/es/blog"},{"@type":"ListItem","position":3,"name":"TCP vs QUIC vs HTTP/3: Kernel Bypass, Zero-Copy y Optimización de Latencia","item":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"¿QUIC es siempre más rápido que TCP?","acceptedAnswer":{"@type":"Answer","text":"No. En redes de baja latencia y poca pérdida, TCP con kernel bypass (io_uring o DPDK) suele igualar o superar a QUIC porque QUIC corre en userspace y tiene overhead de encriptación. QUIC brilla en conexiones con pérdida, alta latencia y escenarios de migración de conexión."}},{"@type":"Question","name":"¿Qué es kernel bypass y cuándo debería usarlo?","acceptedAnswer":{"@type":"Answer","text":"Kernel bypass permite que la aplicación envíe y reciba paquetes sin pasar por el stack de networking del SO. Tecnologías como DPDK e io_uring reducen cambios de contexto y copias de memoria. Se recomienda cuando se necesita latencia sub-milisegundo o se procesan millones de paquetes por segundo."}},{"@type":"Question","name":"¿Cómo funciona zero-copy networking con io_uring?","acceptedAnswer":{"@type":"Answer","text":"La operación IORING_OP_SEND_ZC de io_uring pasa referencias a buffers al kernel en vez de copiar datos. El kernel hace DMA directo desde el buffer de aplicación a la NIC. Se recibe un evento de completion cuando el buffer se puede reutilizar."}}]}]}],"contentHash":"ccdc03817f8bca431b8919b37eb46f77d0422a44c868c5bb4582292e371b6a31","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/es/blog/tcp-quic-http3-kernel-bypass-performance","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":65258,"contentBytes":12844}}
