Tutoriales de Servidores • • 5-8 minutos

HTTP/3 en Nginx: activa QUIC y comprueba que el navegador lo está usando

Diego Cortés
Diego Cortés
Full Stack Developer & SEO Specialist
Compartir:
HTTP/3 en Nginx: activa QUIC y comprueba que el navegador lo está usando
Imagen generada con IA

Tienes HTTP/2, el certificado en regla y un VPS que va fino, y aun así tu web sigue negociando por TCP mientras los móviles de tus usuarios pierden paquetes. HTTP/3 en Nginx se activa en diez líneas; el problema son tres pasos que, si los saltas, no fallan: simplemente no hacen nada.

Qué cambia HTTP/3 de verdad

HTTP/3 no es una versión más rápida de HTTP/2 sobre el mismo transporte: cambia el transporte. Corre sobre QUIC, que viaja en datagramas UDP y resuelve en el cliente buena parte de lo que antes hacía la red.

QUIC sobre UDP: la conexión deja de depender del orden de los paquetes

En TCP, si se pierde un paquete, todo lo que viene detrás espera aunque ya haya llegado: es la cabeza de cola bloqueada. QUIC numera los flujos por separado, así que un paquete perdido frena su flujo y no los demás. En una red con pérdida —móvil, wifi de hotel, tren— se nota; en fibra estable, casi no.

TLS 1.3 obligatorio y 0-RTT: por qué dejarlo apagado

QUIC integra el cifrado en el protocolo: no hay HTTP/3 sin TLS, y el único válido es 1.3. El handshake y la negociación criptográfica viajan juntos, y eso ahorra un viaje de ida y vuelta frente a TCP más TLS. 0-RTT permite mandar datos en el primer paquete de una conexión repetida, pero esos datos pueden repetirse por un replay, así que solo es seguro con peticiones idempotentes.

Paso 0: comprobar si tu nginx trae el módulo

Es el paso que casi todos se saltan y el que produce el error más confuso: la configuración es válida, pero nginx no arranca porque el binario no sabe qué es QUIC.

nginx -V y la búsqueda de --with-http_v3_module

El módulo es ngx_http_v3_module, existe desde la 1.25.0 y no viene compilado por defecto: hay que habilitarlo con --with-http_v3_module.

nginx -v
nginx -V 2>&1 | tr ' ' '\n' | grep -i http_v3

Si el segundo comando imprime el flag, tienes soporte. Si no imprime nada, el módulo no está y ningún listen ... quic va a funcionar.

Si no está: paquetes oficiales o recompilar

Los paquetes de las distribuciones suelen venir sin el módulo. La salida rápida es el repositorio oficial de nginx.org, que sí lo incluye; la otra es compilar con el flag, pero entonces tú te haces cargo de las actualizaciones de seguridad. Las ramas en curso son 1.28.x (stable) y 1.29.x (mainline): comprueba la tuya con nginx -v en lugar de asumir un número.

Paso 1: la configuración mínima que funciona

server {
    listen 443 ssl;
    listen 443 quic reuseport;

    http2 on;

    server_name ejemplo.com;

    ssl_certificate     /etc/letsencrypt/live/ejemplo.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ejemplo.com/privkey.pem;

    # El encabezado que hace que el navegador descubra h3.
    add_header Alt-Svc 'h3=":443"; ma=86400' always;

    quic_retry on;

    root /var/www/ejemplo.com/public;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
}

listen 443 ssl y listen 443 quic en el mismo server

Los dos son necesarios: uno atiende a quien llega por TCP (HTTP/1.1 y HTTP/2) y el otro a quien llega por UDP con QUIC. Si quitas el de TCP, los navegadores sin soporte y los rastreadores se quedan fuera.

http2 on y reuseport: las dos líneas que se olvidan

Desde la 1.25.1, el HTTP/2 se activa con http2 on; y ya no con el parámetro http2 dentro de listen: si copias una configuración vieja, el HTTP/2 puede quedar desactivado sin que lo notes. reuseport es lo que permite al sistema repartir los datagramas entre varios workers; sin él, un solo proceso atiende todo el tráfico QUIC o la configuración no arranca.

Paso 2: Alt-Svc, el encabezado que hace el trabajo

Aquí está el malentendido más común: tener QUIC escuchando en UDP 443 no hace que nadie lo use. El navegador llega por TCP, lee la respuesta y solo si encuentra Alt-Svc prueba HTTP/3 en la siguiente conexión.

h3=":443" y el ma=86400 que casi nadie explica

h3=":443" anuncia que el mismo origen está disponible por HTTP/3 en el puerto 443. ma es el max-age en segundos: cuánto recuerda el navegador ese anuncio. 86400 son 24 horas; muy alto y los clientes siguen intentando h3 después de que apagues QUIC, muy bajo y hay que redescubrirlo en cada visita.

Dónde va el add_header

A nivel de server, no dentro de un location. Si declaras cualquier add_header en un location, los del server dejan de heredarse en ese bloque: esas páginas salen sin Alt-Svc y el navegador nunca migra. Compruébalo con curl -sI.

Varios dominios en el mismo servidor

Solo puede haber un listen ... quic reuseport por dirección y puerto. Con diez dominios en la misma IP, el primero lleva reuseport y los demás solo quic; si repites el flag, nginx rechaza la configuración al recargar.

Paso 3: abrir el camino a UDP

El firewall es el tercer paso que falla en silencio: el TCP está abierto desde el primer día, el UDP no, y el tráfico QUIC se descarta sin error visible.

ufw, nftables y el 443/udp que nadie abre

sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose

Si el servidor está detrás de un firewall del proveedor cloud, hay que abrirlo también en su panel, no solo en la máquina.

Docker: publicar 443/udp además del TCP

docker run -d -p 443:443/tcp -p 443:443/udp nginx:mainline

En Docker, -p 443:443 publica solo TCP. El síntoma del olvido es engañoso: la web funciona, Alt-Svc aparece en la respuesta y el navegador nunca consigue hablar h3.

Paso 4: verificar que es HTTP/3

Ver alt-svc en la respuesta solo demuestra que lo anuncias. La prueba real es forzar h3 y leer el protocolo con el que respondió el servidor.

curl --http3 y --http3-only: la diferencia importa

curl --version | grep HTTP3
curl -sI https://ejemplo.com | grep -i alt-svc
curl --http3-only -s -o /dev/null -w '%{http_version}\n' https://ejemplo.com

--http3 intenta h3 y cae a versiones anteriores si falla, así que un resultado limpio no prueba nada: puede haber entrado por TCP. --http3-only exige h3 y falla en caso contrario. Un 3 en la salida es la confirmación.

El log de nginx con $server_protocol

log_format proto '$remote_addr "$request" $status proto=$server_protocol';
access_log /var/log/nginx/proto.log proto;

Si al navegar aparecen líneas con proto=HTTP/3.0, el camino completo funciona. Si todas dicen HTTP/2.0, el encabezado no está llegando o el UDP está bloqueado.

tcpdump: ver el tráfico UDP salir de verdad

sudo tcpdump -ni any udp port 443 -c 10

Con el comando corriendo, abre la web desde el navegador. Si no ves paquetes, el problema está antes del servidor: firewall del proveedor, NAT o una red que filtra UDP.

Dónde se rompe (y si tienes un CDN delante)

Redes que bloquean UDP y el mito del fallback

Muchas redes corporativas y algunos operadores filtran UDP en el 443. HTTP/3 está diseñado para eso: el cliente lo intenta, falla y vuelve a TCP sin que el usuario note nada. Por eso el listen 443 ssl no se quita nunca.

MTU y peticiones que no son idempotentes

QUIC evita la fragmentación en el camino, pero si un equipo intermedio descarta los paquetes de descubrimiento de MTU, la conexión se queda reintentando: es la causa típica de "en mi casa va y en la oficina de un cliente no". Y si activas ssl_early_data on sin entender el replay, un POST puede ejecutarse dos veces: en una tienda, un cobro duplicado.

Con un CDN, esto no se configura en tu nginx

Con Cloudflare u otro CDN, el visitante habla HTTP/3 con el CDN y el CDN habla lo que quiera con tu origen. Activar QUIC en tu nginx no cambia lo que ve el usuario.

¿Mi web gana algo? Cómo medirlo sin engañarte

Dónde se nota y dónde no

El beneficio aparece en conexiones nuevas y redes inestables: una visita desde el móvil con cobertura regular ahorra el handshake y no se bloquea con el primer paquete perdido. Donde no se nota es en el backend: QUIC termina en nginx, el tráfico a PHP-FPM sigue siendo FastCGI y la base de datos sigue igual. Si tu TTFB lo marca una consulta lenta, HTTP/3 no la arregla.

Qué medir antes y después

Mide la primera visita en móvil con caché vacía, antes y después, y compara percentiles en lugar de promedios. Lo que no puedes prometer es un porcentaje: no existe un benchmark universal y cada red se comporta distinto.

Conclusión

HTTP/3 es barato de activar y fácil de romper en silencio. La diferencia entre "lo activé" y "funciona" son tres comprobaciones: que el módulo esté compilado, que Alt-Svc salga en la respuesta y que curl --http3-only devuelva un 3. En el blog tenemos además la guía de deploy de Laravel con Nginx y la de caché de página para bajar el TTFB, que atacan el otro extremo del mismo problema.

Categorías