
🌐 También en: English · Deutsch · Français
Muchos usuarios de mis instancias de Nextcloud se conectan desde el móvil. Justo para eso se creó HTTP/3: funciona sobre QUIC en lugar de TCP, necesita un viaje de ida y vuelta menos para establecer la conexión, soporta mejor la pérdida de paquetes y puede mantener la conexión cuando el teléfono pasa del wifi a los datos móviles. Pero mi objetivo no era «tener HTTP/3», sino HTTP/3 solo si ayuda de forma medible: más rendimiento o una conexión más estable en una red mala.
Este artículo describe cómo lo activé, cómo demuestro que de verdad funciona y los dos frenos que al principio hacían HTTP/3 claramente más lento que HTTP/2.
🔌 El controlador no puede; su nginx, sí
Mi ingress es el NGINX Ingress Controller de F5. No tiene ninguna opción para HTTP/3: ni interruptor, ni clave de ConfigMap, ni nada en las notas de versión. Pero un vistazo dentro del pod en ejecución muestra que el nginx 1.31.7 incluido está compilado con --with-http_v3_module, junto con OpenSSL 3.5, la primera versión de OpenSSL que admite QUIC en el lado del servidor. Y como el controlador funciona con hostNetwork, nginx puede abrir directamente UDP 443.
Así que se hace con los snippets que ya uso:
# server-snippets of the main site (once per address):
listen 443 quic reuseport rcvbuf=4m sndbuf=4m;
listen [::]:443 quic reuseport rcvbuf=4m sndbuf=4m;
# http-snippets of the controller:
map $scheme $http3_alt_svc {
https 'h3=":443"; ma=604800';
default "";
}
http3_stream_buffer_size 1m;
quic_gso on;
# next to the other security headers (location-snippets):
add_header Alt-Svc $http3_alt_svc always;reuseportsolo una vez por dirección: nginx solo permite esta opción en una única línealistenpor dirección y puerto. Por eso solo está en el bloque de servidor del sitio principal, y todos los demás hosts comparten esos sockets.- Alt-Svc solo por HTTPS: esta cabecera indica a los navegadores que existe HTTP/3. El
mapsolo la rellena en HTTPS; en el puerto 80 el valor queda vacío y nginx no envía ninguna cabecera. - Alt-Svc junto a las demás cabeceras: nginx deja de heredar
add_headeren cuanto una location define sus propias cabeceras. Mis cabeceras de seguridad están en los location-snippets, así que Alt-Svc tiene que ir ahí también. - Cortafuegos: UDP 443 de entrada para IPv4 e IPv6. De salida no hace falta ninguna regla: las respuestas son «established» para conntrack.
Todo depende de un interruptor por host (ingress_http3_enabled), desactivado por defecto. Primero activé el blog y después la instancia de Nextcloud más pequeña. Antes de la ejecución real comprobé las líneas nuevas con nginx -t sobre una copia de la configuración dentro del pod.
Lo que queda: «no admitido» a veces solo significa «no tiene interruptor». nginx -V muestra lo que el binario puede hacer de verdad.
🤔 Por qué pensé tanto tiempo que HTTP/3 era imposible
Ya lo había comprobado en agosto, con el resultado «no se puede». Eso decía desde entonces un comentario en mi plantilla del cortafuegos. Entonces comprobé dos cosas: ¿tiene el controlador una opción para HTTP/3? No. ¿Responde el servidor por HTTP/3? No, claro: no había ningún listener QUIC.
Lo que no comprobé: si el nginx del controlador incluye el módulo. La conclusión «no hay opción, así que no hay soporte» fue demasiado rápida. Hoy ya no se puede saber si la imagen de entonces tenía el módulo; la actual sí, junto con una versión de OpenSSL que admite QUIC en el servidor. Solo la pregunta «¿qué puede hacer el binario?» en lugar de «¿qué ofrece la configuración?» llevó a la otra respuesta.
Lo que queda: un «no se puede» merece una fecha y un motivo, y una segunda comprobación cuando el software ha cambiado.
✅ Cómo se ve que HTTP/3 funciona de verdad
Una página que carga no dice nada del protocolo. Hice estas comprobaciones después de cada activación:
curl -sI --http2 https://www.apt-upgrade.me/ | grep -i alt-svc
alt-svc: h3=":443"; ma=604800
curl -s --http3-only -o /dev/null -w '%{http_code} h%{http_version}\n' https://www.apt-upgrade.me/
200 h3- HTTP/3 forzado con
curl --http3-onlyen todas las rutas importantes: portada, inicio de sesión, elstatus.phpde Nextcloud, la API OCS, WebDAV, Collabora; en todas, la respuesta esperada por h3. - Mismo contenido: un archivo JavaScript grande descargado por HTTP/2 y por HTTP/3, con la misma suma SHA-256. Las cabeceras de respuesta también son las mismas.
- Lado del servidor:
ss -ulnpmuestra un socket UDP por worker de nginx en el puerto 443, en IPv4 e IPv6; en el registro de accesos aparecen peticiones conHTTP/3.0. - Desde fuera: http3check.net confirma QUIC versión 1 y HTTP/3, probado por IPv6, algo que desde casa no puedo comprobar.
- La seguridad no cambia: los filtros de fail2ban reconocen las líneas de inicio de sesión con
HTTP/3.0igual que conHTTP/2.0. Los datos tempranos 0-RTT siguen desactivados, para que nadie pueda repetir un formulario de inicio de sesión.
Una pequeña trampa: mi primera prueba de fail2ban falló, con una línea de registro hecha a mano y en el formato equivocado. Con una línea real del registro, en la que solo se había cambiado el protocolo, el filtro coincidió enseguida.
Lo que queda: una prueba necesita varios ángulos: el cliente, el servidor, un tercero desde fuera y las propias herramientas de seguridad.
🐢 Freno 1: 64 KB por viaje de ida y vuelta
Las peticiones pequeñas fueron igual de rápidas o más rápidas por HTTP/3 desde el principio. Luego llegó la prueba con un archivo grande: 5,7 MB tardaban 0,18 segundos por HTTP/2 y 0,99 segundos por HTTP/3, más de cinco veces más.
El cliente apenas usaba CPU; estaba esperando. Las cuentas llevaron a la causa: por defecto, nginx solo guarda 64 KB por flujo QUIC (http3_stream_buffer_size). Un flujo no puede tener más datos en tránsito. 64 KB por viaje de ida y vuelta a 11 ms dan exactamente los 5,8 MB/s medidos. En una conexión móvil de 50 ms serían solo unos 1,3 MB/s, así que HTTP/3 habría frenado a mis usuarios fuera de casa.
Con un búfer de 1 MB, las descargas por HTTP/3 alcanzaron 27–31 MB/s, a la par que HTTP/2.
Lo que queda: un protocolo nuevo trae valores por defecto nuevos. Bastó una sola prueba con un archivo grande para encontrar el freno; por eso forma parte de toda prueba de aceptación.
🧺 Freno 2: un búfer de socket que se desborda
Después, la otra dirección: una subida de 20 MB. Por HTTP/3 llegaban de media 7,8 MB/s, por HTTP/2 11,9 MB/s, y mientras tanto el servidor contó 284 paquetes UDP descartados (RcvbufErrors en /proc/net/snmp).
La razón está en la diferencia con TCP. Con TCP, el kernel gestiona por sí mismo el flujo y los búferes. QUIC, en cambio, se ejecuta en la aplicación, sobre UDP: cada paquete espera en el búfer de recepción del socket hasta que nginx lo recoge. Por defecto, Linux da a un socket como máximo 208 KB; una subida rápida lo llena y el kernel tira los paquetes siguientes. QUIC tiene que reenviarlos y se frena a sí mismo.
La solución tiene dos partes:
- Subir el límite del kernel:
net.core.rmem_maxynet.core.wmem_maxa 7,5 MB, el valor que recomiendan las implementaciones de QUIC. Solo sube el techo; todos los demás sockets conservan su valor por defecto. - Pedir los búferes: los listeners de QUIC piden por sí mismos búferes más grandes con
rcvbuf=4m sndbuf=4m. El kernel duplica el valor internamente; después,ss -mmuestra 8 MB por socket.
Después: 0 paquetes descartados, y las subidas por HTTP/3 quedaron al nivel de HTTP/2; las variaciones restantes venían de mi propia conexión.
Lo que queda: quien mueve UDP a alto rendimiento tiene que conocer los búferes de sus sockets. El kernel cuenta los paquetes descartados; solo hay que mirar.
📊 Las mediciones
Medí desde mi homelab con curl, 15 conexiones nuevas por valor (mediana). El blog está más lejos (unos 42 ms por viaje de ida y vuelta), el Nextcloud más cerca (unos 11 ms).
| Medición | HTTP/2 | HTTP/3 |
|---|---|---|
| Blog: conexión + cifrado listos | 96 ms | 58 ms |
| Blog: primer byte de la portada | 139 ms | 104 ms |
| Blog: portada completa | 268 ms | 227 ms |
| Nextcloud: conexión + cifrado listos | 31 ms | 27 ms |
| Nextcloud: descarga de 5,7 MB, búfer por defecto | 34 MB/s | 5,8 MB/s |
| Nextcloud: descarga de 5,7 MB, búfer de flujo de 1 MB | 28 MB/s | 30 MB/s |
| Nextcloud: subida de 20 MB, búfer de socket de 208 KB | 11,9 MB/s | 7,8 MB/s |
| Nextcloud: subida de 20 MB, búfer de socket de 8 MB | 15,1 MB/s | 13,0 MB/s |
| Nextcloud: subida de 100 MB, búfer de socket de 8 MB | 8,7 MB/s | 7,7 MB/s |
| Collabora: petición discovery (47 KB) | 55–72 ms | 57 ms |
Con 100 MB, mi propia conexión de subida limita igual a los dos protocolos, sin paquetes descartados. No se pudieron probar descargas más grandes sin una cuenta de usuario: el archivo público más grande del Nextcloud es el script de 5,7 MB. Collabora edita los documentos por WebSockets, que siguen por TCP; ahí solo se puede medir la petición discovery.
La ventaja al establecer la conexión es un viaje de ida y vuelta: TCP y TLS necesitan dos, QUIC hace ambas cosas en uno. En el blog, a 42 ms, son unos 40 ms por conexión nueva; en el Nextcloud cercano, casi nada. En redes móviles de 50 a 100 ms la ganancia es mayor en proporción.
Lo que queda: HTTP/3 ahorra sobre todo tiempo de espera, no ancho de banda. Cuanto más lejos está el usuario, más ayuda.
🔧 Los demás ajustes
quic_gso on: nginx entrega al kernel varios paquetes QUIC de una vez en lugar de uno por llamada. Ahorra CPU al enviar; mis tarjetas de red virtuales lo hacen por software, lo que igualmente ayuda. Activado.- Alt-Svc durante 7 días en lugar de 1: los visitantes habituales empiezan directamente por HTTP/3 durante más tiempo. Si más adelante se desactiva HTTP/3, cada navegador pierde como mucho un intento y vuelve por sí solo a HTTP/2. Ajustado.
quic_bpf: se asegura de que, tras un cambio de red, los paquetes de una conexión lleguen al worker de nginx correcto. Para ello nginx tiene que cargar un programa eBPF, lo que exige permisos adicionales (CAP_BPF,CAP_NET_ADMIN), justo en el contenedor expuesto a Internet, que a propósito solo tiene un permiso. No activado: si una migración falla, el móvil simplemente vuelve a conectarse, con QUIC en un solo viaje de ida y vuelta.- 0-RTT (
ssl_early_data): ahorraría otro viaje en las conexiones recurrentes, pero esas peticiones se pueden repetir, también los inicios de sesión. Sigue desactivado.
Lo que queda: cada ajuste tiene un precio. Se activa lo que cuesta poco; lo que debilita el endurecimiento se queda fuera, aunque sobre el papel sea más rápido.
🧭 Desde la primera visita: el registro HTTPS en el DNS
Alt-Svc tiene un defecto: el navegador solo se entera de que hay HTTP/3 con la primera respuesta. Por eso la primerísima visita siempre va por HTTP/2. Un registro DNS propio de tipo HTTPS (RFC 9460) lo soluciona: le dice al navegador, ya al resolver el nombre, qué protocolos habla el servidor.
# DNS zone (example): HTTPS record, priority 1, "." = same name
www.example.org. 3600 IN HTTPS 1 . alpn="h3,h2"
# check it:
dig +short HTTPS www.example.org
1 . alpn="h3,h2"El 1 es la prioridad, el punto significa «el mismo nombre» y alpn enumera los protocolos en el orden preferido. Renuncio a propósito a las pistas de IP, porque habría que mantenerlas con cada cambio de dirección. Chrome, Firefox y Safari tienen en cuenta el registro. No todas las interfaces de gestión de DNS ofrecen este tipo; conviene comprobarlo antes de buscar por qué falta.
Ese es justo mi caso: la gestión de DNS de Strato, donde está apt-upgrade.me, solo ofrece registros A, AAAA, CNAME, MX, NS, TXT y SRV, así que ahí no puedo crear un registro HTTPS. Tampoco se puede imitar: los navegadores solo tienen en cuenta el tipo real. Por eso el blog se queda con Alt-Svc: la primera visita va por HTTP/2 y cada una de las siguientes, durante siete días, directamente por HTTP/3. Cambiar de proveedor de DNS solo por esto no me compensaría.
Lo que queda: el servidor solo puede ofrecer HTTP/3. Que el navegador lo use ya en la primera visita lo decide el DNS.
📱 Qué aporta a mis usuarios y qué no
- Navegadores en el móvil: la mayor ganancia: conexión más rápida, sin bloqueo de todas las peticiones cuando se pierde un paquete, y la conexión sobrevive al cambio entre wifi y datos móviles.
- App de Nextcloud en el iPhone: iOS usa HTTP/3 por sí solo en cuanto el servidor lo anuncia con Alt-Svc.
- App de Android y cliente de escritorio: por lo que sé, usan bibliotecas de red sin HTTP/3. El registro de accesos mostrará la proporción real en unos días.
- Collabora y notify_push: funcionan con WebSockets. nginx no admite WebSockets sobre HTTP/3, así que esas conexiones siguen en HTTP/1.1 sobre TCP; funciona, solo que sin la ventaja.
Lo que no pude medir desde aquí es el comportamiento en una red mala. Esa será la próxima prueba: con el móvil en una zona de poca cobertura, comparando HTTP/3 activado y desactivado.
Lo que queda: ser honesto sobre a quién beneficia. Una mejora de protocolo solo ayuda a los clientes que lo hablan.
📅 Estado y próximos pasos
HTTP/3 ya funciona en los tres servidores: el blog y las dos instancias de Nextcloud. La segunda instancia la validé con las mismas comprobaciones que la primera, y la comprobación de deriva da changed=0 en todas partes: los servidores coinciden exactamente con el código del repositorio público.
Dentro de una semana revisaré tres cosas:
- Uso: qué parte de las peticiones del registro de accesos llega por
HTTP/3.0, separando navegadores, apps de Nextcloud y clientes de sincronización. - Errores: si hay más respuestas de error o subidas interrumpidas por HTTP/3 que por HTTP/2.
- Práctica: cómo se comporta Nextcloud en el móvil con poca cobertura, con y sin HTTP/3.
Publicaré las cifras en los próximos siete días en un artículo aparte. Si HTTP/3 apenas aporta nada a mis usuarios o muestra más errores, lo desactivo servidor por servidor: un interruptor y una ejecución del playbook.
🎯 Las lecciones
- Comprobar primero lo que puede hacer el binario: «sin opción» no significa «sin módulo».
- Probar con archivos grandes y en ambas direcciones: las peticiones pequeñas esconden cualquier freno al rendimiento.
- Los búferes UDP forman parte de QUIC: los
RcvbufErrorsmuestran si el kernel descarta paquetes. - Probar con líneas de registro reales: un ejemplo hecho a mano solo prueba el ejemplo.
- Activarlo porque ayuda: medido, no supuesto, y con un interruptor que se puede volver a desactivar.





