Volver al Blog
30 min Copiar como Markdown
Parte 3 / 5 · Endurecer Nginx, desde el borde hacia dentro

Dominando QUIC y HTTP/3 con Nginx: La Guía Completa

QUIC hace que cada stream sea independiente en la capa de transporte, así que un paquete perdido solo detiene el suyo y no todos, como pasa en HTTP/2 sobre TCP. Nginx necesita 1.25.0+ y el UDP/443 abierto, o cae a HTTP/2 sin avisar.

Imagen de portada de Dominando QUIC y HTTP/3 con Nginx: La Guía Completa

La web ha dependido de TCP durante más de cuatro décadas. Pero las aplicaciones modernas —con sus exigencias de interactividad en tiempo real, conectividad móvil y cargas de página instantáneas— han expuesto las limitaciones fundamentales de TCP. Llegan QUIC y HTTP/3: una reinvención completa del transporte web que abandona TCP en favor de UDP para ofrecer un internet más rápido, seguro y resiliente.

En esta guía completa, exploraremos la historia y la arquitectura de QUIC, entenderemos por qué resuelve problemas que TCP no puede, y recorreremos una configuración completa de Nginx lista para producción.

TL;DR — QUIC y HTTP/3 en Nginx

6 puntos clave
  • QUIC funciona sobre UDP, no TCP: HTTP/3 mapea la semántica HTTP sobre QUIC, que se construye sobre UDP y exige TLS 1.3.
  • Resuelve el HOL blocking: los streams de QUIC son independientes en la capa de transporte, así que un paquete perdido solo bloquea su propio stream, no todos los streams multiplexados como en HTTP/2 sobre TCP.
  • Handshakes más rápidos y migración de conexión: handshake combinado de transporte+criptografía (1-RTT, o 0-RTT al reanudar) más Connection IDs que sobreviven a los cambios de red (Wi-Fi → datos móviles).
  • Nginx necesita 1.25.0+ con --with-http_v3_module y una biblioteca SSL con soporte QUIC (OpenSSL 3.5.1+, BoringSSL, QuicTLS o LibreSSL 3.6+).
  • Configuración base: añade listen 443 quic reuseport;, activa http3 on, TLS 1.3 y la cabecera Alt-Svc; reuseport debe aparecer en exactamente un listener por dirección:puerto.
  • Abre UDP/443 en el firewall (servidor y proveedor cloud) o QUIC fallará silenciosamente y los clientes retrocederán a HTTP/2; verifica con curl --http3, las DevTools o quic="h3" en el log de acceso.

Cómo lo ejecuto en mi propia infraestructura

jmrp.io sirve HTTP/3 en sí mismo: los dos server blocks en producción llevan listen 443 quic; junto a los listeners TCP habituales, de modo que el sitio acepta conexiones QUIC y TLS-sobre-TCP en paralelo, según lo que soporte realmente el cliente — y lo que haya entre el cliente y el servidor.

No me fié de la palabra de la cabecera Alt-Svc. Ejecutar curl --http3 directamente contra el sitio en vivo devuelve HTTP/3 200: un handshake QUIC completado, no una inferencia sacada de una cabecera de respuesta que un servidor mal configurado podría seguir enviando mientras cae en silencio a HTTP/2 por debajo. Esa distinción importa — muchas listas de comprobación de “HTTP/3 activado” se quedan en la cabecera y nunca confirman que el protocolo se negoció de verdad. Es un detalle pequeño de verificar, y también es la única forma de saber que un despliegue QUIC es real y no aspiracional: la cabecera no cuesta nada añadir, pero solo un handshake desde el lado del cliente demuestra que UDP/443 está realmente abierto y negociando de extremo a extremo.

Servir HTTP/3 y que se use son dos preguntas distintas, y solo la segunda necesita datos. jmrp.io está detrás de Cloudflare, que termina la conexión del cliente, así que el protocolo que negocia de verdad un visitante aparece en la analítica de zona de Cloudflare y no en mi propio log de acceso. En ocho días —del 19 al 27 de agosto de 2026, filtrando por el host jmrp.io para que el resto de la zona no lo tape— HTTP/3 se llevó el 33,5 % de las peticiones que negociaron HTTP/2 o HTTP/3: 11.208 de 33.411.

Ese denominador pide dos frases de defensa, porque la versión honesta de esta medición va sobre todo de lo que hay que tirar. Excluí HTTP/1.1 por completo, y la exclusión no es cosmética: los navegadores actuales acuerdan h2 por ALPN contra Cloudflare, así que una petición que dice ser un navegador y aun así llega por HTTP/1.1 es casi siempre un scraper con el user-agent falseado. Y había unos cuantos: un cubo de «Chrome de escritorio» salía con un 45,8 % de HTTP/1.1, que es algo que Chrome de escritorio no hace. Quitar HTTP/1.1 elimina las falsificaciones evidentes, pero no garantiza que el resto esté limpio: el plan Free no expone puntuación de bot, y la dimensión del navegador se deduce del propio user-agent, de modo que no queda ningún campo independiente por el que filtrar. Por eso tampoco hay aquí una tabla de adopción por navegador: no voy a publicar porcentajes por familia construidos sobre un denominador que no puedo auditar. La ventana son ocho consultas encadenadas de 24 horas, el rango más largo que devuelve ese plan, con lo que arrastra ocho costuras de dos minutos: unos 16 minutos ausentes en total.

Dos cosas sobre esa cifra. La primera es que es una instantánea de esa ventana, no una propiedad estable del sitio. La misma consulta sobre los siete días UTC completos del 26 de agosto al 1 de septiembre de 2026 da un 20,1 % —10.508 de 52.341— y oscila de un día a otro:

Cuota de HTTP/3 por día — host jmrp.io, del 26 de agosto al 1 de septiembre de 2026
Día (UTC)HTTP/3HTTP/2 + HTTP/3Cuota de HTTP/3
2026-08-262.0139.04622,3 %
2026-08-271.76510.45716,9 %
2026-08-281.6307.14222,8 %
2026-08-298826.90212,8 %
2026-08-301.5446.42324,0 %
2026-08-311.2726.77818,8 %
2026-09-011.4025.59325,1 %
Total10.50852.34120,1 %

La segunda es que ninguna de las dos lecturas se puede recalcular después. El plan Free retiene httpRequestsAdaptiveGroups ocho días en ventana deslizante y rechaza cualquier consulta más antigua con cannot request data older than 1w1d, así que ambas cifras son observaciones fechadas que la propia fuente no volverá a servir. La tabla está aquí por eso, y publica los dos recuentos y no solo el porcentaje diario, porque los porcentajes por sí solos no bastarían: el día con más tráfico casi duplica al más tranquilo, así que las siete cuotas redondeadas no permiten reconstruir el 20,1 % ponderado —su media sin ponderar es del 20,4 %—. Con cada numerador y cada denominador en la página, el total es aritmética que cualquiera puede comprobar.

Un detalle medido que no esperaba. Mi origen sí envía la cabecera que esta guía te dice que añadas —add_header Alt-Svc 'h3=":443"; ma=86400' always; está en los dos server blocks TLS, y una petición directa al origen vuelve con ella—. A través de Cloudflare, esa cabecera sencillamente no está:

Alt-Svc en el origen frente al edge
En la propia máquina de origen, saltándose el CDN por completo:$ curl -sS -kD- -o /dev/null -H 'Host: jmrp.io' https://127.0.0.1/
alt-svc: h3=":443"; ma=86400
$ curl -sS -D- -o /dev/null --resolve jmrp.io:443:104.21.40.251 https://jmrp.io/
server: cloudflare
(sin cabecera alt-svc en la respuesta)
$ dig +short HTTPS jmrp.io @1.1.1.1
1 . alpn="h3,h2" ipv4hint=104.21.40.251,172.67.158.146 ech=…

QUIC funciona igualmente, como ya demostraba el handshake de antes, porque el anuncio vive en otro sitio: el registro DNS HTTPS del dominio lleva alpn="h3,h2", y los navegadores que resuelven ese registro se enteran de HTTP/3 antes de abrir una sola conexión. Esto es comportamiento de Cloudflare en toda su plataforma, no una particularidad de mi configuración — el edge termina la conexión y decide por su cuenta qué anuncia, y nada de mi configuración de Nginx puede cambiarlo. La lección práctica es estrecha pero vale: si tienes un CDN delante del origen que configuraste para HTTP/3, la cabecera que estás comprobando puede no ser la que ven tus visitantes. Mira también el registro DNS.


¿Por qué QUIC sustituyó a TCP en HTTP/3?

El protocolo QUIC tiene una evolución interesante desde un experimento propietario de Google hasta un estándar IETF completo.

Cronología de QUIC
AñoHitoImportancia
2012Google comienza el desarrollo de QUICProyecto interno para reducir la latencia web
2013Primer tráfico QUIC en ChromePrimeros experimentos con servicios de Google
2014Despliegue a gran escala de gQUICChrome de escritorio usa QUIC para las propiedades de Google
2016Se forma el Grupo de Trabajo QUIC del IETFComienza el proceso de estandarización formal
2017IETF QUIC diverge de gQUICIntegración de TLS 1.3, transporte de propósito general
2021Se publica el RFC 9000Protocolo de transporte QUIC estandarizado
2022Se publica el RFC 9114HTTP/3 estandarizado

La familia de RFCs de QUIC

La especificación completa de QUIC abarca múltiples RFCs:

Documentos estándar de QUIC
RFCTítuloDescripción
RFC 9000QUIC TransportProtocolo base: paquetes, frames, streams, gestión de conexiones
RFC 9001Using TLS to Secure QUICIntegración de TLS 1.3, derivación de claves, niveles de cifrado
RFC 9002Loss Detection and Congestion ControlRecuperación de paquetes perdidos, estimación de RTT, algoritmos de congestión
RFC 8999Version-Independent PropertiesComportamientos comunes a todas las versiones de QUIC
RFC 9114HTTP/3Semántica HTTP sobre QUIC

¿Por qué QUIC? Entendiendo las limitaciones de TCP

Para apreciar QUIC, debemos entender por qué TCP —la columna vertebral de internet desde 1974— tiene dificultades con las demandas modernas de la web.

¿Qué es el head-of-line (HOL) blocking?

Este es el problema fundamental que QUIC resuelve. Imagina una autopista con un solo carril (conexión TCP). Si un coche se avería (pérdida de paquete), todos los que van detrás deben parar y esperar, aunque vayan a destinos diferentes.

ClienteRedServidorClienteRedServidor**HTTP/2 sobre TCP: HOL Blocking**TODOS los streams bloqueadosesperando retransmisión**HTTP/3 sobre QUIC: Streams Independientes**Solo Stream A espera¡B y C continúan!Stream A: Paquete 1Stream B: Paquete 1Stream A: Paquete 2 (PERDIDO)Stream C: Paquete 1Stream A: Paquete 1Stream B: Paquete 1 (¡BLOQUEADO!)(Esperando Stream A: Paquete 2...)Stream A: Paquete 1Stream B: Paquete 1Stream A: Paquete 2 (PERDIDO)Stream C: Paquete 1Stream A: Paquete 1Stream B: Paquete 1 (¡Entregado!)Stream C: Paquete 1 (¡Entregado!)
Head-of-Line Blocking: TCP vs QUIC

El problema de la latencia del handshake

Establecer una conexión TCP segura requiere múltiples viajes de ida y vuelta:

Comparativa de establecimiento de conexión
ProtocoloConexión nuevaConexión reanudadaRTTs totales
TCP + TLS 1.2TCP handshake + TLS handshakeTCP + TLS abreviado3 RTT → 2 RTT
TCP + TLS 1.3TCP handshake + TLS 1.3TCP + TLS 0-RTT2 RTT → 1 RTT
QUICHandshake combinado0-RTT early data1 RTT → 0 RTT

El problema de la migración de conexión

Las conexiones TCP se identifican por una 4-tupla: (IP origen, puerto origen, IP destino, puerto destino). Cuando cambias de Wi-Fi a datos móviles, tu IP cambia — y tu conexión TCP se rompe.


Arquitectura de QUIC en profundidad

Transporte sobre UDP

QUIC se construye sobre UDP en lugar de crear un nuevo protocolo IP. Fue una elección pragmática:

  • No requiere cambios en el kernel: UDP tiene soporte universal
  • Implementación en espacio de usuario: Iteración y despliegue más rápidos
  • Traversal de middleboxes: UDP atraviesa la mayoría de NATs y firewalls

Estructura de paquetes

QUIC utiliza dos tipos de paquetes:

Tipos de paquetes QUIC
TipoCabeceraUsoCifrado
Long HeaderInformación completa de conexiónInitial, Handshake, 0-RTT, RetryClaves específicas por nivel
Short HeaderMínima (post-handshake)Datos de aplicación (1-RTT)Claves de aplicación

Streams y control de flujo

Los streams QUIC son canales ligeros y multiplexados dentro de una conexión:

No

0 (Par)

1 (Impar)

0 (Bidi)

1 (Uni)

0 (Bidi)

1 (Uni)

No

Frame de Stream entrante

¿Límite MAX_DATA de conexión OK?

Conexión bloqueada

Decodificar Stream ID

¿Último bit: Iniciador?

Iniciado por cliente

Iniciado por servidor

¿Penúltimo bit: Dirección?

¿Penúltimo bit: Dirección?

Tipo 0x0: Cliente Bidi

Tipo 0x2: Cliente Uni

Tipo 0x1: Servidor Bidi

Tipo 0x3: Servidor Uni

¿Límite MAX_STREAM_DATA OK?

Stream bloqueado

Buffer de recepción

Lógica de procesamiento de streams QUIC

Decodificación del Stream ID: El Stream ID es un entero de 62 bits donde los 2 bits menos significativos funcionan como cabecera de tipo:

  • Bit 0 (Iniciador): 0 = Cliente, 1 = Servidor
  • Bit 1 (Dirección): 0 = Bidireccional, 1 = Unidireccional

Esto crea 4 espacios de direcciones distintos:

  • …00: Petición/Respuesta del cliente
  • …01: Server Push (obsoleto)
  • …10: Stream de control del cliente
  • …11: Stream de control del servidor (QPACK)

Control de flujo de doble capa: Para que un paquete sea procesado, debe pasar dos comprobaciones independientes:

  1. Nivel de conexión: ¿Hay crédito MAX_DATA para toda la conexión?
  2. Nivel de stream: ¿Hay crédito MAX_STREAM_DATA para este stream específico? Si cualquiera se agota, la transmisión se bloquea hasta que llegue un frame WINDOW_UPDATE.

El handshake de QUIC: velocidad y seguridad

QUIC combina los handshakes de transporte y criptográficos en un único intercambio, reduciendo drásticamente la latencia.

Handshake 1-RTT (primera conexión)

ServidorClienteServidorCliente**Handshake 1-RTT de QUIC**Contiene SCID, DCID, versiónEl servidor envía las claves de datos de aplicación¡Puede enviar petición HTTP inmediatamente!Total: 1 viaje de ida y vuelta hasta los primeros datosPaquete InitialClientHello + Parámetros de transporte QUICPaquetes Initial + HandshakeServerHello + Cert + FinishedHandshake completo + Primera peticiónDatos de respuesta
Handshake 1-RTT de QUIC

Handshake 0-RTT (conexión reanudada)

Para clientes que se han conectado previamente, QUIC permite enviar datos cifrados en el primer paquete:

ServidorClienteServidorCliente**QUIC 0-RTT (Conexión reanudada)**¡Datos enviados antes de completar el handshake!Total: 0 viajes de ida y vuelta para enviar la peticiónPaquetes Initial + 0-RTTClientHello + Early Data (HTTP GET)Initial + Handshake + 1-RTTServerHello + Cert + Datos de respuesta
Reanudación 0-RTT de QUIC

Arquitectura de seguridad

La seguridad de QUIC no es opcional —está integrada en el protocolo desde su diseño.

Cuatro niveles de cifrado

Niveles de cifrado de QUIC
NivelClaves derivadas deUsoForward Secrecy
InitialDestination Connection IDPrimeros paquetes, negociación de versiónNo
0-RTTClave precompartida (PSK)Datos de aplicación tempranosNo
HandshakeSecretos del handshake TLSCompletar el handshakeParcial
Application (1-RTT)Secretos de tráfico TLSTodos los datos post-handshakeCompleto (ECDHE)

Protección de paquetes

Cada paquete QUIC (excepto Initial) está protegido con cifrado AEAD:

Estructura y protección de paquetes QUIC
Sección del paqueteComponenteNivel de protección
CabeceraFlagsProtegido (Header Protection)
Connection IDTexto claro (para enrutamiento)
Número de paqueteCifrado (Header Protection)
Carga útilDatos de aplicaciónTotalmente cifrado (AEAD)
PieEtiqueta de autenticaciónMAC de integridad de 16 bytes

Connection ID y privacidad

Mecanismos de protección contra DoS

QUIC incluye varias medidas anti-amplificación y anti-suplantación:

  1. Validación de dirección: Los servidores envían paquetes RETRY para validar las direcciones de los clientes
  2. Anti-amplificación: Los servidores limitan los datos enviados antes de la validación de dirección (3x los datos del cliente)
  3. Stateless Reset: Terminación limpia de conexión sin estado

HTTP/3: HTTP sobre QUIC

HTTP/3 es el mapeo de la semántica HTTP sobre el transporte QUIC. Reemplaza el framing binario de HTTP/2 con streams QUIC.

HTTP/2 vs HTTP/3
CaracterísticaHTTP/2HTTP/3
TransporteTCP + TLSQUIC (UDP + TLS 1.3)
MultiplexingFrames de stream en una única conexiónStreams nativos de QUIC
HOL BlockingSí (en la capa TCP)No (streams independientes)
Compresión de cabecerasHPACKQPACK (maneja el desorden)
Server PushSoportadoObsoleto (poco usado)
Migración de conexiónNo

¿Quién usa HTTP/3 hoy?

A fecha de 2026, la adopción de HTTP/3 es significativa y creciente:

Estadísticas de adopción de HTTP/3
MétricaValorFuente
Sitios web usando HTTP/336,6%W3Techs (2026)
Uso de QUIC en Chrome (subsiguiente)40%APNIC Labs
Mejora en carga de página (global)12,4%Cloudflare Benchmarks
Mejora en regiones de alta latencia13,8%DebugBear

Configuración de Nginx: guía completa

Ahora vamos a implementar HTTP/3 en Nginx. Esta sección cubre todo, desde los prerrequisitos hasta el despliegue en producción.

Prerrequisitos

Verifica tu instalación de Nginx:

nginx -V 2>&1 | grep -E 'version|http_v3_module|OpenSSL|BoringSSL'
Salida de la versión de Nginx
nginx version: nginx/1.28.0
built with OpenSSL 3.5.0 8 Apr 2025 (running with OpenSSL 3.5.4 30 Sep 2025)
configure arguments: … --with-http_v3_module …
Cómo instalar Nginx con soporte HTTP/3

Los paquetes que nginx.org publica para Debian y Ubuntu vienen compilados --with-http_v3_module contra OpenSSL 3.5, así que HTTP/3 no exige compilar nada a mano. Los mismos comandos valen para las dos distribuciones: /etc/os-release aporta el fabricante y el nombre en clave.

sudo apt install curl gnupg2 ca-certificates
DISTRO=$(. /etc/os-release && echo "$ID")
CODENAME=$(. /etc/os-release && echo "$VERSION_CODENAME")

# Clave de firma de Nginx en su propio llavero
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://nginx.org/keys/nginx_signing.key -o /tmp/nginx.key

# Contrasta las huellas con la lista que publica nginx en
# https://nginx.org/en/pgp_keys.html ANTES de confiar en la clave. El
# paquete lleva varias claves y rotan, así que compara la salida y no un
# número copiado de un artículo.
gpg --show-keys --with-fingerprint /tmp/nginx.key

sudo gpg --dearmor -o /etc/apt/keyrings/nginx.gpg /tmp/nginx.key

# Repositorio mainline, restringido a ese llavero
echo "deb [signed-by=/etc/apt/keyrings/nginx.gpg] https://nginx.org/packages/mainline/$DISTRO $CODENAME nginx" \
    | sudo tee /etc/apt/sources.list.d/nginx.list

sudo apt update && sudo apt install nginx

# Comprobar la compilación que acabas de instalar
nginx -V 2>&1 | tr ' ' '\n' | grep -E 'http_v3|OpenSSL'

Solo hace falta si quieres un módulo que los paquetes no traen. Nginx compila HTTP/3 contra cualquier biblioteca SSL que exponga una API QUIC: OpenSSL 3.5+, BoringSSL o LibreSSL 3.6+. En una distribución actual basta con el OpenSSL del sistema — el propio paquete oficial de Debian 13 está compilado contra OpenSSL 3.5.6.

openssl version   # 3.5.0 o superior

wget https://nginx.org/download/nginx-1.31.4.tar.gz
tar xzf nginx-1.31.4.tar.gz
cd nginx-1.31.4

./configure \
    --with-http_v3_module \
    --with-http_ssl_module \
    --with-http_v2_module

make -j"$(nproc)"
sudo make install

Configuración básica

Esta es la configuración mínima para habilitar HTTP/3:

/​etc/​nginx/​sites-available/​example.com.conf
server {
    server_name example.com;
    root /var/www/example.com;

    # ==========================================
    # LISTENERS: TCP (HTTP/1.1, HTTP/2) + UDP (HTTP/3)
    # ==========================================
    
    # Standard HTTPS over TCP
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    
    # QUIC/HTTP/3 over UDP
    # 'reuseport' is critical for multi-worker performance
    listen 443 quic reuseport;
    listen [::]:443 quic reuseport;

    # ==========================================
    # SSL/TLS CONFIGURATION
    # ==========================================
    
    # TLS 1.3 is REQUIRED for QUIC
    ssl_protocols TLSv1.3 TLSv1.2;
    ssl_prefer_server_ciphers off;
    
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # ==========================================
    # HTTP/3 CONFIGURATION
    # ==========================================
    
    http3 on;
    
    # Advertise HTTP/3 support to browsers
    # ma=86400 means "cache this info for 24 hours"
    add_header Alt-Svc 'h3=":443"; ma=86400' always;

    location / {
        try_files $uri $uri/ =404;
    }
}

Configuración completa de producción

Aquí tienes una configuración exhaustiva con todas las optimizaciones. La configuración se divide en dos archivos: el nginx.conf principal para ajustes globales y un archivo de configuración específico del sitio.

/​etc/​nginx/​nginx.conf 42 líneas
# Main context
worker_processes auto;
error_log /var/log/nginx/error.log warn;

events {
    worker_connections 4096;
    use epoll;
    multi_accept on;
}

http {
    # ==========================================
    # HTTP/3 GLOBAL SETTINGS
    # ==========================================
    # Explicit for clarity (http3 defaults to 'on' in nginx 1.25.0+)
    http3 on;
    
    # Custom log format to track HTTP/3 connections
    log_format quic '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    '"$http_referer" "$http_user_agent" '
                    'proto="$server_protocol" quic="$http3"';

    # MIME types
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # Performance optimizations
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    
    # Gzip compression
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css application/json application/javascript 
               text/xml application/xml application/xml+rss text/javascript;

    # Include site configurations
    include /etc/nginx/sites-enabled/*;
}

La configuración específica del sitio incluye todas las directivas para QUIC, TLS, cabeceras y logging:

/​etc/​nginx/​sites-available/​example.com.conf 104 líneas
server {
    server_name example.com www.example.com;
    root /var/www/example.com;

    # ==========================================
    # DUAL-STACK LISTENERS
    # ==========================================
    
    # TCP: HTTP/1.1 and HTTP/2 fallback
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    
    # UDP: QUIC/HTTP/3
    listen 443 quic reuseport;
    listen [::]:443 quic reuseport;

    # ==========================================
    # TLS CONFIGURATION (Required for QUIC)
    # ==========================================
    
    ssl_protocols TLSv1.3 TLSv1.2;
    # TLS 1.3 ciphersuites (handled separately for better OpenSSL compatibility)
    ssl_conf_command Ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256;
    # TLS 1.2 ciphers only (ECDHE for forward secrecy)
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
    ssl_prefer_server_ciphers off;
    ssl_ecdh_curve X25519:P-256:P-384;
    
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    
    # Session resumption for performance
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;  # Better security
    
    # OCSP Stapling
    ssl_stapling on;
    ssl_stapling_verify on;

    # ==========================================
    # QUIC-SPECIFIC SETTINGS
    # ==========================================
    
    # Enable 0-RTT early data (with security considerations)
    ssl_early_data on;
    
    # DoS protection: require address validation
    quic_retry on;
    
    # Performance: Generic Segmentation Offload (Linux 4.18+)
    quic_gso on;

    # ==========================================
    # HEADERS
    # ==========================================
    
    # Advertise HTTP/3 availability
    add_header Alt-Svc 'h3=":443"; ma=86400' always;
    
    # Warn backends about 0-RTT replay risk
    add_header Early-Data $ssl_early_data always;
    
    # Security headers
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "DENY" always;

    # ==========================================
    # LOGGING
    # ==========================================
    
    access_log /var/log/nginx/example.com.access.log quic;
    error_log /var/log/nginx/example.com.error.log;

    # ==========================================
    # LOCATIONS
    # ==========================================
    
    location / {
        try_files $uri $uri/ =404;
    }
    
    # Static assets with long cache
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
        expires 1y;
        add_header Cache-Control "public, immutable" always;
        add_header Alt-Svc 'h3=":443"; ma=86400' always;
        
        # Security headers (inherited from server block may not apply with add_header in location)
        add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-Frame-Options "DENY" always;
    }
}

# HTTP to HTTPS redirect
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

Referencia de todas las directivas QUIC

Directivas QUIC de Nginx
DirectivaPor defectoContextoDescripción
http3onhttp, serverHabilita la negociación del protocolo HTTP/3
http3_hqoffhttp, serverHabilita HTTP/0.9 sobre QUIC (solo para pruebas)
http3_max_concurrent_streams128http, serverMáximo de streams concurrentes por conexión
http3_stream_buffer_size64khttp, serverTamaño del buffer para lectura/escritura de streams
quic_active_connection_id_limit2http, serverMáximo de Connection IDs almacenados por conexión
quic_bpfoffmainEnrutamiento eBPF para migración de conexión (Linux 5.7+)
quic_gsooffhttp, serverGeneric Segmentation Offload (Linux 4.18+)
quic_host_key-http, serverArchivo con clave secreta para tokens de validación de dirección
quic_retryoffhttp, serverHabilita la validación de dirección mediante paquetes Retry

Configuración del firewall

Crítico: HTTP/3 usa UDP en el puerto 443, no TCP. Si tu firewall solo permite TCP/443, QUIC fallará silenciosamente y los clientes retrocedarán a HTTP/2.

# Check current rules
sudo ufw status

# Allow UDP on port 443
sudo ufw allow 443/udp comment 'QUIC/HTTP3'

# Verify
sudo ufw status verbose
# Allow incoming UDP 443
sudo iptables -A INPUT -p udp --dport 443 -j ACCEPT

# Save rules (Debian/Ubuntu)
sudo iptables-save | sudo tee /etc/iptables/rules.v4

# For IPv6
sudo ip6tables -A INPUT -p udp --dport 443 -j ACCEPT
sudo ip6tables-save | sudo tee /etc/iptables/rules.v6
# Add UDP 443 to default zone
sudo firewall-cmd --permanent --add-port=443/udp

# Reload
sudo firewall-cmd --reload

# Verify
sudo firewall-cmd --list-ports

Añade una regla de entrada:

  • Tipo: Custom UDP
  • Rango de puertos: 443
  • Origen: 0.0.0.0/0 (o tu CIDR)
  • Descripción: QUIC/HTTP3

Ajuste del kernel para alto tráfico

Para servidores con alto tráfico, aumenta los tamaños de buffer UDP:

/​etc/​sysctl.d/​99-quic.conf
# Increase UDP buffer sizes for QUIC
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.core.rmem_default = 1048576
net.core.wmem_default = 1048576

# UDP memory limits
net.ipv4.udp_mem = 65536 131072 262144
net.ipv4.udp_rmem_min = 8192
net.ipv4.udp_wmem_min = 8192

# Allow more local ports for connections
net.ipv4.ip_local_port_range = 1024 65535

Aplica los ajustes:

sudo sysctl --system

Verificación y pruebas

Tras la configuración, verifica que HTTP/3 funciona correctamente.

Método 1: curl

Las versiones modernas de curl (7.66+ con soporte HTTP/3) permiten probar directamente:

# Test HTTP/3 specifically
curl -I --http3-only https://jmrp.io
# Or allow fallback
curl -I --http3 https://jmrp.io
Salida de la respuesta HTTP/3 con curl
HTTP/3 200
server: jmrp.io
date: Wed, 14 Jan 2026 20:02:32 GMT
content-type: text/html; charset=utf-8
alt-svc: h3=":443"; ma=86400
strict-transport-security: max-age=63072000; includeSubDomains; preload

Método 2: DevTools del navegador

  1. Abre Chrome o Firefox
  2. Navega a tu sitio
  3. Abre DevTools (F12) → pestaña Red (Network)
  4. Haz clic derecho en las cabeceras de columna → Activa la columna Protocolo
  5. Recarga la página
  6. Busca h3 en la columna Protocolo

Método 3: herramientas online

Herramientas de prueba HTTP/3
HerramientaURLCaracterísticas
HTTP/3 Checkhttp3check.netTest rápido de aprobado/suspenso con detalles
Cloudflare HTTP/3 Testcloudflare-quic.comPrueba de conexión en vivo
Qualys SSL Labsssllabs.comAnálisis exhaustivo de TLS

Método 4: comprobar los logs de Nginx

Usa el formato de log personalizado para verificar el tráfico HTTP/3:

# Check for HTTP/3 connections
grep 'quic="h3"' /var/log/nginx/access.log | tail -5
Salida del log mostrando tráfico HTTP/3
192.168.1.100 - - [14/Jan/2026:20:02:32 +0000] "GET / HTTP/3" 200 15234 "-" "Mozilla/5.0…" proto="HTTP/3" quic="h3"

Guía de resolución de problemas

Problemas comunes de HTTP/3 y soluciones
SíntomaCausa probableSolución
El protocolo se queda en HTTP/2Firewall bloqueando UDP/443Abre el puerto UDP 443 en el servidor y en el proveedor cloud
El protocolo se queda en HTTP/2Falta la cabecera Alt-SvcAñade add_header Alt-Svc 'h3=":443"; ma=86400' always;
Errores de conexiónTLS 1.3 no habilitadoHabilita TLSv1.3 para QUIC (obligatorio); TLSv1.2 puede seguir habilitado como fallback para HTTP/2
Los navegadores se niegan a usar QUICCertificado autofirmadoUsa un certificado válido (Let’s Encrypt)
Nginx no arrancaFalta --with-http_v3_moduleRecompila Nginx con el módulo HTTP/3
Uso alto de CPUGSO no soportado por el kernelDesactiva quic_gso o actualiza el kernel
0-RTT no funcionaVersión de OpenSSL demasiado antiguaUsa OpenSSL 3.5.1+, QuicTLS o BoringSSL

Modo depuración

Activa el logging de depuración temporalmente:

nginx
error_log /var/log/nginx/error.log debug;

Comprueba los mensajes específicos de QUIC:

grep -i quic /var/log/nginx/error.log | tail -20
Log de depuración de Nginx (fallo simulado)
2026/01/14 10:42:15 [debug] 12345#0: *1 quic handle packet: fd:16, addr:192.0.2.1:54321
2026/01/14 10:42:15 [debug] 12345#0: *1 quic packet rx dcid len:8 87654321
2026/01/14 10:42:15 [debug] 12345#0: *1 quic packet rx scid len:8 12345678
2026/01/14 10:42:15 [info] 12345#0: *1 quic SSL_do_handshake() failed: SSL_ERROR_SSL: error:14094416:SSL routines:ssl3_read_bytes:sslv3 alert certificate unknown
2026/01/14 10:42:15 [debug] 12345#0: *1 quic close connection: 0:

Lista de verificación para producción

Antes de desplegar HTTP/3 en producción, verifica:

Lista de verificación para HTTP/3 en producción
CategoríaElementoVerificación
BuildNginx compilado con --with-http_v3_modulenginx -V 2>&1 | grep http_v3
BuildBiblioteca SSL compatible (QuicTLS/BoringSSL/OpenSSL 3.5.1+)nginx -V 2>&1 | grep -i ssl
RedUDP/443 abierto en el firewall del servidorsudo ss -ulnp | grep 443
RedUDP/443 abierto en el proveedor cloud (AWS/GCP/Azure)Comprueba las reglas del security group/firewall
Configlisten 443 quic reuseport; presentenginx -T | grep quic
ConfigTLS 1.3 habilitadonginx -T | grep ssl_protocols
ConfigCabecera Alt-Svc configuradacurl -I https://example.com | grep alt-svc
CertificadoCertificado válido (no autofirmado)openssl s_client -connect site:443
PruebaHTTP/3 confirmado y funcionandocurl --http3 https://example.com
MonitorizaciónFormato de log incluye $http3Comprueba la configuración del formato de log

¿Cuándo NO deberías usar QUIC?

Aunque QUIC ofrece beneficios significativos, hay escenarios donde TCP puede ser preferible:

Preguntas frecuentes

¿Qué necesito para habilitar HTTP/3 en Nginx?

Necesitas Nginx 1.25.0 o superior (el soporte HTTP/3 se añadió a mediados de 2023) compilado con --with-http_v3_module, además de una biblioteca SSL con soporte QUIC como OpenSSL 3.5.1+, BoringSSL, QuicTLS o LibreSSL 3.6+. TLS 1.3 es obligatorio para QUIC.

¿HTTP/3 usa TCP o UDP?

HTTP/3 funciona sobre QUIC, que se construye sobre UDP en lugar de TCP. Debes abrir el puerto UDP 443 tanto en el firewall del servidor como en el del proveedor cloud, o QUIC fallará silenciosamente y los clientes retrocederán a HTTP/2.

¿Cómo resuelve QUIC el head-of-line blocking de TCP?

TCP ve todo como un único flujo ordenado de bytes, así que un solo paquete perdido bloquea todos los streams multiplexados de HTTP/2 hasta su retransmisión. QUIC implementa streams en la capa de transporte, dando entrega independiente a cada stream, por lo que un paquete perdido solo afecta a su propio stream.

¿Por qué mi navegador sigue mostrando HTTP/2 en la primera visita?

Ver HTTP/2 en la primera visita es lo esperado: un navegador que nunca ha conectado con el origen no tiene forma de saber que habla HTTP/3, así que abre primero una conexión TCP. Durante esa petición descubre HTTP/3 —por la cabecera de respuesta Alt-Svc o por un registro DNS HTTPS que anuncia alpn=h3— y cambia a HTTP/3 en las peticiones siguientes o tras recargar.

¿Por qué es crítico el flag reuseport para QUIC en Nginx?

UDP no tiene conexión, así que sin reuseport todos los paquetes QUIC los maneja un único worker de Nginx, creando un cuello de botella. La función reuseport del kernel distribuye los paquetes entre los workers, pero debe aparecer en exactamente una directiva listen por dirección:puerto a nivel global o Nginx no arrancará.

¿Cómo verifico que HTTP/3 funciona?

Usa curl moderno con curl -I --http3 https://tusitio, activa la columna Protocolo en las DevTools del navegador y busca h3, usa herramientas online como http3check.net o cloudflare-quic.com, o filtra el log de acceso de Nginx por quic="h3".

Lecturas y recursos adicionales

  1. RFC 9000 IETF
  2. RFC 9001 IETF
  3. RFC 9002 IETF
  4. RFC 8999 IETF
  5. RFC 9114 IETF
  6. W3Techs (2026) w3techs.com
  7. APNIC Labs blog.apnic.net
  8. Cloudflare Benchmarks blog.cloudflare.com
  9. DebugBear debugbear.com
  10. http3 Nginx
  11. http3_hq Nginx
  12. http3_max_concurrent_streams Nginx
  13. http3_stream_buffer_size Nginx
  14. quic_active_connection_id_limit Nginx
  15. quic_bpf Nginx
  16. quic_gso Nginx
  17. quic_host_key Nginx
  18. quic_retry Nginx
  19. http3check.net http3check.net
  20. cloudflare-quic.com cloudflare-quic.com
  21. ssllabs.com Qualys SSL Labs