Skip to content

Keep-Alive no es Cache-Control: anatomía de una cascada de red

Published:

En una auditoría me encontré con un patrón que parece inofensivo hasta que miras la cascada de red: dos imágenes pesadas, de 608 KB y 403 KB, servidas bajo HTTP/1.1, con la cabecera Connection: Keep-Alive presente y ausencia total de Cache-Control. A primera vista alguien podría pensar que el Keep-Alive ya está “cacheando” algo. No cachea nada. Son dos capas distintas que resuelven problemas distintos, y confundirlas se paga en cada visita.

Vamos a ver qué hace realmente cada cabecera, por qué Keep-Alive ayuda pero no basta en HTTP/1.1, cómo HTTP/2 cambia las reglas con la multiplexación, y dónde encaja <link rel="preconnect"> para adelantarse al handshake. Al final hay una demo interactiva donde puedes recorrer los cuatro escenarios y ver qué cambia, y qué no, en cada uno de ellos.

El hallazgo en la auditoría

Las dos respuestas compartían las mismas cabeceras; la de la imagen más pesada tenía esta pinta:

❌ Bad: respuesta real auditada
HTTP/1.1 200 OK
Content-Type: image/jpeg
Content-Length: 622592
Connection: Keep-Alive

Dos cosas saltan a la vista:

  1. Hay Connection: Keep-Alive, así que la conexión se reutiliza. Útil, pero eso es la conexión; no guarda ni un byte para la próxima visita.
  2. No hay Cache-Control por ningún lado. Esto significa que quien navega vuelve a descargar más de 1 MB de imágenes en cada visita, e incluso al navegar entre páginas si el navegador decide revalidar. Un megabyte que podría vivir en caché durante meses se descarga una y otra vez.

La cabecera que de verdad importa para las visitas recurrentes brillaba por su ausencia:

✅ Good: lo que debería servir el origen
HTTP/2 200
content-type: image/jpeg
content-length: 622592
cache-control: public, max-age=31536000, immutable

El error mental detrás de esto suele ser pensar que Keep-Alive y Cache-Control van de lo mismo. No. Operan en capas diferentes.

Capa de red vs capa de almacenamiento

La distinción es esta:

Una analogía: Keep-Alive es mantener abierta la tubería del agua para no tener que volver a conectar el grifo a la red en cada vaso. Cache-Control es llenar una jarra y dejarla en la nevera para no abrir el grifo siquiera. Son optimizaciones complementarias, no sustitutas.

Connection: Keep-AliveCache-Control
CapaRed (conexión TCP)Almacenamiento (caché del cliente)
Qué reutilizaEl tubo abiertoLos bytes ya descargados
ÁmbitoDentro de una cargaEntre cargas y visitas
Si faltaRe-handshake por peticiónRe-descarga del recurso completo

En el caso auditado teníamos lo primero pero no lo segundo. Reutilizábamos el tubo, pero tirábamos los datos a la basura después de cada visita. El arreglo más barato y de mayor impacto era añadir Cache-Control con un max-age largo e immutable para assets versionados.

El impacto de Keep-Alive en HTTP/1.1

Dejando la caché a un lado, miremos qué hace Keep-Alive durante esa primera carga, porque aquí hay un matiz que mucha gente da por hecho y es falso.

El problema de fondo de HTTP/1.1 es que una conexión solo procesa una petición a la vez: no hay multiplexación. Para descargar las dos imágenes a la vez, el navegador abre dos conexiones en paralelo hacia el dominio de assets (el límite son unas 6 por origen). Cada conexión paga su propio peaje: handshake TCP (SYN, SYN-ACK, ACK) y, sobre HTTPS, también el handshake TLS. Son dos handshakes, en paralelo, antes de poder descargar.

Aquí está el matiz: con dos imágenes concurrentes, Connection: close y Connection: keep-alive tardan lo mismo en esta primera carga. En ambos casos el navegador abre dos conexiones nuevas y negocia dos handshakes; no hay ninguna conexión abierta previa hacia assets que reutilizar. La diferencia es el ciclo de vida: con close las conexiones se cierran al terminar; con keep-alive se quedan abiertas en el pool.

¿Dónde se nota entonces el keep-alive? En la siguiente petición a ese mismo origen: otra imagen al hacer scroll, una navegación, una llamada a la API. Esa petición reutiliza una conexión abierta y se ahorra el handshake. Es exactamente el mismo malentendido que con la caché: igual que Keep-Alive no cachea nada, tampoco acelera la primera carga, solo las posteriores.

Resumiendo el comportamiento de HTTP/1.1:

La evolución hacia HTTP/2 y el superpoder de preconnect

Con HTTP/2 esto cambia. Sobre una única conexión introduce multiplexación: cada recurso viaja como un stream independiente, y muchos streams comparten la misma conexión al mismo tiempo. Las dos imágenes ya no necesitan dos conexiones con dos handshakes: viajan por la misma, en paralelo, y desaparece el límite de 6 conexiones por origen.

Esto tiene una consecuencia directa: Connection: Keep-Alive deja de tener sentido en HTTP/2. La cabecera ni siquiera es válida en el protocolo; la conexión persistente y multiplexada es el comportamiento por defecto. Si ves Keep-Alive en una respuesta, es una pista de que sigues en HTTP/1.1.

Ahora bien, incluso en HTTP/2 queda un coste que la multiplexación no elimina: el handshake inicial hacia el dominio de assets. El navegador no descubre que necesita ese dominio hasta que parsea el HTML y encuentra las imágenes. Solo entonces empieza a resolver DNS, abrir TCP y negociar TLS. Esos round trips se cuelan en la ruta crítica, justo antes de las descargas.

Aquí entra <link rel="preconnect">. Es un resource hint que le dice al navegador: “vas a necesitar este origen, ve abriendo la conexión ya”.

<link rel="preconnect" href="https://assets.example.com" crossorigin />

Colocado en el <head>, el navegador ejecuta el handshake TCP/TLS hacia assets.example.com en paralelo con la descarga y el parseo del HTML. Cuando llega el momento de pedir las imágenes, la conexión ya está abierta: el handshake desaparece de la ruta crítica y las descargas empiezan al instante.

Un par de matices que comprobé en la auditoría:

Visualízalo: la cascada en cuatro estados

Cambia entre los cuatro escenarios y compara cuántas conexiones TCP se abren, cuántos handshakes hay en la ruta crítica y el tiempo total de cada caso. Fíjate especialmente en dónde aparece, o desaparece, la barra roja de handshake.

Modelo realista: documento en el origen principal e imágenes en un dominio de assets. Los tiempos son estimaciones didácticas con bloques fijos (handshake 100 ms, descarga proporcional al peso: 608 KB ≈ 220 ms, 403 KB ≈ 150 ms) para aislar el efecto de cada optimización.

Lo interesante es lo que no cambia. Para dos imágenes, close, keep-alive y HTTP/2 tardan prácticamente lo mismo en la primera carga (unos 570 ms en el modelo): el cambio de protocolo reduce conexiones (de 3 a 2) y handshakes en la ruta crítica (de 2 a 1), pero no recorta el tiempo total. El único que lo recorta es preconnect, que adelanta el handshake y baja a 470 ms. La multiplexación de HTTP/2 brilla de verdad cuando hay decenas de recursos, donde el techo de 6 conexiones de HTTP/1.1 empieza a serializar peticiones. Y ojo, todo esto es solo la primera visita; con un buen Cache-Control, la segunda ni siquiera toca la red para estas imágenes.

Conclusión y buenas prácticas

Volviendo al hallazgo original, estas eran las correcciones que aplicaban a ese caso real:

Cada vez veremos menos Keep-Alive: con HTTP/2 y HTTP/3 como norma, la cabecera ni siquiera aplica. Pero todavía nos lo encontramos en aplicaciones antiguas y en infraestructuras de países con limitaciones de recursos, así que conviene saber leerlo cuando aparece en una auditoría.

Keep-Alive y Cache-Control no compiten: trabajan en capas distintas. Cuando entiendes que una gestiona el tubo y la otra los datos, dejas de confundir una conexión reutilizada con un recurso cacheado, y empiezas a optimizar las dos cosas a la vez.

Notas

  1. Lo defino como “formatos modernos” por consistencia con la información que nos ofrece Lighthouse en sus tests, pero el formato WebP se publicó el 30 de septiembre de 2010 y AVIF el 19 de febrero de 2019. Para mí, un formato moderno es JPEG-XL, del cual quiero escribir un post para analizarlo en profundidad.

Next Post
El misterio del #document que se repinta solo: Paint flashing y capas de composición