HTTP 103 Early Hints es una respuesta informativa que el servidor envía antes de la respuesta definitiva. Lleva cabeceras Link con rel=preload o rel=preconnect, de modo que el navegador empieza a descargar el CSS, las fuentes o a abrir conexiones con otros dominios mientras la aplicación todavía está generando el HTML. En este tutorial pondrás una aplicación de ejemplo que emite 103 detrás de Nginx en Ubuntu 24.04, activarás la directiva early_hints y comprobarás con curl que el 103 llega al cliente.

Early Hints es útil cuando el servidor tarda en generar la página (consultas a base de datos, renderizado en servidor). Si tu HTML sale en pocos milisegundos, la mejora será mínima.

Cómo funciona Early Hints en Nginx

Nginx no genera respuestas 103 por sí mismo. Desde la versión 1.29.0 puede reenviar al cliente los 103 que emite el backend cuando actúa como proxy inverso, y la directiva early_hints decide para qué peticiones se hace. El flujo es este:

  1. El navegador pide / a Nginx.
  2. Nginx reenvía la petición a tu aplicación con proxy_pass.
  3. La aplicación responde enseguida con 103 Early Hints y cabeceras Link, y sigue trabajando.
  4. Nginx reenvía el 103 al navegador, que empieza a precargar los recursos.
  5. La aplicación termina y envía el 200 OK con el HTML.

Por eso necesitas dos piezas: una aplicación o framework capaz de emitir 103 (Node.js, por ejemplo, con response.writeEarlyHints()) y Nginx 1.29.0 o posterior. Ubuntu 24.04 trae Nginx 1.24, así que instalarás el paquete oficial de nginx.org.

Requisitos previos

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath.
  • Un usuario no root con privilegios sudo.
  • Un dominio (en esta guía, your_domain) con un registro DNS A apuntando a la IP del servidor. Early Hints solo se envía a clientes HTTP/2 o HTTP/3, y los navegadores solo usan HTTP/2 sobre TLS.
  • Los puertos 80 y 443 abiertos. Con UFW: sudo ufw allow 'Nginx Full' después de instalar Nginx, o sudo ufw allow 80,443/tcp.

Paso 1: Instalar Nginx desde el repositorio oficial

Si tienes instalado el Nginx de Ubuntu, elimínalo primero (tus ficheros de /etc/nginx se conservan con remove, pero haz copia de tus sitios si los tienes):

sudo apt remove nginx nginx-common

Instala las herramientas para añadir el repositorio:

sudo apt update
sudo apt install curl gnupg2 ca-certificates lsb-release ubuntu-keyring

Descarga la clave de firma de nginx.org en /etc/apt/keyrings:

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://nginx.org/keys/nginx_signing.key | sudo gpg --dearmor -o /etc/apt/keyrings/nginx-archive-keyring.gpg

Añade el repositorio de la rama mainline, que es la que incorpora antes las funciones nuevas como early_hints:

echo "deb [signed-by=/etc/apt/keyrings/nginx-archive-keyring.gpg] https://nginx.org/packages/mainline/ubuntu $(lsb_release -cs) nginx" | sudo tee /etc/apt/sources.list.d/nginx.list

Da prioridad a este repositorio sobre el de Ubuntu para que apt no instale la versión 1.24:

printf 'Package: *\nPin: origin nginx.org\nPin: release o=nginx\nPin-Priority: 900\n' | sudo tee /etc/apt/preferences.d/99nginx

Instala Nginx, arráncalo y comprueba la versión:

sudo apt update
sudo apt install nginx
sudo systemctl enable --now nginx
nginx -v
nginx version: nginx/1.29.x

Cualquier versión igual o superior a 1.29.0 sirve. El paquete de nginx.org no usa sites-available/sites-enabled: los sitios se configuran en /etc/nginx/conf.d/*.conf.

Paso 2: Crear una aplicación de ejemplo que emita 103

Para probar el flujo de forma reproducible usarás un pequeño servidor en Node.js que envía un 103 y tarda medio segundo en generar la página, como haría una aplicación real que consulta una base de datos. El Node.js de Ubuntu 24.04 (versión 18) ya incluye writeEarlyHints():

sudo apt install nodejs

Crea el directorio de la aplicación y el fichero principal:

sudo mkdir -p /opt/demo-hints
sudo nano /opt/demo-hints/server.js
const http = require('node:http');

const links = [
  '</css/app.css>; rel=preload; as=style',
  '</fonts/inter.woff2>; rel=preload; as=font; type="font/woff2"; crossorigin',
  '<https://cdn.your_domain>; rel=preconnect',
];

http.createServer((req, res) => {
  // 1. Enviar el 103 en cuanto llega la petición
  res.writeEarlyHints({ link: links });

  // 2. Simular 500 ms de trabajo en el servidor
  setTimeout(() => {
    res.writeHead(200, {
      'Content-Type': 'text/html; charset=utf-8',
      // Repetir los Link en el 200 para clientes que ignoran el 103
      'Link': links.join(', '),
    });
    res.end('<!doctype html><html><head><link rel="stylesheet" href="/css/app.css"></head><body><h1>Hola</h1></body></html>');
  }, 500);
}).listen(3000, '127.0.0.1');

Crea una unidad de systemd para ejecutarla como servicio con un usuario sin privilegios:

sudo nano /etc/systemd/system/demo-hints.service
[Unit]
Description=Demo HTTP 103 Early Hints
After=network.target

[Service]
ExecStart=/usr/bin/node /opt/demo-hints/server.js
DynamicUser=yes
Restart=on-failure

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now demo-hints

Comprueba que la aplicación envía el 103 directamente, sin pasar por Nginx:

curl -sv -o /dev/null http://127.0.0.1:3000/ 2>&1 | grep -E '^< (HTTP|[Ll]ink)'
< HTTP/1.1 103 Early Hints
< Link: </css/app.css>; rel=preload; as=style, </fonts/inter.woff2>; rel=preload; as=font; type="font/woff2"; crossorigin, <https://cdn.your_domain>; rel=preconnect
< HTTP/1.1 200 OK
< Link: </css/app.css>; rel=preload; as=style, </fonts/inter.woff2>; rel=preload; as=font; type="font/woff2"; crossorigin, <https://cdn.your_domain>; rel=preconnect

Si usas otro backend, el principio es el mismo: tu framework debe escribir el 103 antes de empezar el trabajo pesado.

Paso 3: Obtener un certificado TLS

Instala Certbot con su plugin de Nginx:

sudo apt install certbot python3-certbot-nginx

Crea primero un sitio mínimo en el puerto 80 para que Certbot pueda validar el dominio:

sudo nano /etc/nginx/conf.d/your_domain.conf
server {
    listen 80;
    server_name your_domain;
}
sudo nginx -t && sudo systemctl reload nginx
sudo certbot certonly --nginx -d your_domain

Certbot guarda el certificado en /etc/letsencrypt/live/your_domain/ y programa su renovación automática.

Paso 4: Activar early_hints en el proxy inverso

La directiva early_hints acepta una o varias variables: si alguna no está vacía y no vale 0, Nginx reenvía el 103 del backend. Los clientes HTTP/1.1 antiguos pueden gestionar mal las respuestas 1xx, así que lo habitual es activarlo solo para HTTP/2 y HTTP/3 con un map. Sustituye el contenido de /etc/nginx/conf.d/your_domain.conf:

sudo nano /etc/nginx/conf.d/your_domain.conf
# Activar Early Hints solo para clientes HTTP/2 o HTTP/3
map $http2$http3 $early_hints {
    ""      0;
    default 1;
}

server {
    listen 80;
    server_name your_domain;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    http2 on;
    server_name your_domain;

    ssl_certificate     /etc/letsencrypt/live/your_domain/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your_domain/privkey.pem;

    location / {
        early_hints $early_hints;

        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_pass http://127.0.0.1:3000;
    }
}

Desde Nginx 1.25.1 HTTP/2 se activa con http2 on; y no con el parámetro http2 de listen. Comprueba la sintaxis y recarga:

sudo nginx -t && sudo systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Paso 5: Verificar que el cliente recibe el 103

Haz una petición HTTP/2 desde tu equipo o desde el propio servidor. curl -v muestra las respuestas informativas:

curl -sv --http2 -o /dev/null https://your_domain/ 2>&1 | grep -E '^< (HTTP|link)'
< HTTP/2 103
< link: </css/app.css>; rel=preload; as=style, </fonts/inter.woff2>; rel=preload; as=font; type="font/woff2"; crossorigin, <https://cdn.your_domain>; rel=preconnect
< HTTP/2 200
< link: </css/app.css>; rel=preload; as=style, </fonts/inter.woff2>; rel=preload; as=font; type="font/woff2"; crossorigin, <https://cdn.your_domain>; rel=preconnect

El 103 aparece antes del 200. Repite la petición forzando HTTP/1.1 para confirmar que el map lo desactiva:

curl -sv --http1.1 -o /dev/null https://your_domain/ 2>&1 | grep -E '^< HTTP'
< HTTP/1.1 200 OK

En el navegador, abre las herramientas de desarrollo de Chrome o Edge, pestaña Network: las peticiones de app.css y inter.woff2 deben arrancar antes de que termine la descarga del documento. Chrome y Edge soportan Early Hints desde la versión 103; el soporte en Firefox y Safari es más reciente y más limitado, y los navegadores que no lo entienden simplemente ignoran el 103.

Paso 6: Elegir bien qué recursos indicar

Cada recurso que precargas compite por ancho de banda con el HTML, así que limita la lista a lo que bloquea el primer renderizado:

Valor de relQué hace el navegadorCuándo usarlo
preloadDescarga el recurso con prioridad altaCSS crítico, fuente principal, imagen LCP
preconnectAbre DNS, TCP y TLS con otro origen, sin descargar nadaCDN de imágenes o fuentes en otro dominio

Reglas prácticas:

  • Indica 2 a 5 recursos como máximo. Precargar todo anula la ventaja.
  • Las fuentes necesitan as=font y crossorigin, aunque sean del mismo dominio, o el navegador las descargará dos veces.
  • Las rutas deben coincidir exactamente con las del HTML (incluido el hash de versión, como /css/app.3f9a1c.css). Si no coinciden, Chrome mostrará en la consola el aviso de recurso precargado sin usar.
  • Mantén las cabeceras Link también en el 200, para clientes que no procesan el 103.

Solución de problemas

El 103 no aparece a través de Nginx pero sí en el puerto 3000. Comprueba la versión con nginx -v (debe ser 1.29.0 o superior) y que la petición es HTTP/2: si curl muestra HTTP/1.1, revisa que el bloque server incluye http2 on;. Confirma también que la directiva está cargada:

sudo nginx -T 2>/dev/null | grep -n early_hints

nginx -t devuelve unknown directive "early_hints". Sigues usando el Nginx de Ubuntu. Revisa que el pin de /etc/apt/preferences.d/99nginx existe y ejecuta apt policy nginx para ver de qué repositorio sale la versión candidata.

Hay una CDN o balanceador delante de Nginx. Muchos proxies descartan las respuestas 1xx. Algunas CDN, como Cloudflare, generan sus propios Early Hints a partir de las cabeceras Link del 200 si activas la función en su panel; en ese caso basta con que tu origen envíe esas cabeceras.

Conclusión

Has instalado Nginx 1.29+ desde el repositorio oficial, has puesto una aplicación que emite 103 Early Hints detrás de él y has activado early_hints solo para clientes HTTP/2 y HTTP/3. A partir de aquí puedes medir el efecto con Lighthouse o WebPageTest comparando el LCP antes y después, aplicar el mismo patrón a tu aplicación real y combinarlo con la compresión Brotli o la caché de Nginx para reducir aún más el tiempo de carga.