Durante semanas sentía que algo iba mal. El código estaba limpio, los endpoints respondían sin errores, la base de datos apenas respiraba y las gráficas de rendimiento parecían normales.

Y aun así, cada vez que lanzaba una petición a mi API, había algo que no cuadraba. No era lento exactamente, pero sí… pesado. Como si cada llamada tuviera que cruzar una puerta que se cerraba y volvía a abrir en cada intento.

Seguramente tú también has pasado por esta experiencia frustrante. Todo lo que uno suele revisar en estos casos ya estaba afinado, índices, cachés, compresión, JSON reducido al mínimo, latencia de base de datos bajo control. Realmente nada explicaba por qué las respuestas oscilaban entre 120 y 150 milisegundos. No es el servidor. No es la lógica.

Era el propio trayecto, el vaivén invisible entre cliente y servidor.

Hasta que descubrimos que la solución era una simple línea. Un header que la mayoría de las veces pasamos por alto, enterrado entre otros que damos por supuestos.

Con esa pequeña instrucción, el cliente y el servidor acuerdan mantener la conexión abierta tras completar la respuesta, permitiendo que futuras solicitudes viajen por el mismo canal TCP/TLS sin repetir el handshake.

El cambio es inmediato. En las pruebas, las peticiones pasaron de unos 128ms a 102ms de media. Un 20% más rápido sin tocar una sola línea de lógica.

Qué estaba pasando

Por defecto, muchos clientes y servidores HTTP aún cierran las conexiones después de cada respuesta, especialmente cuando hay proxies intermedios o configuraciones antiguas. En cada nuevo request se abre un socket, se establece el TCP handshake (tres pasos) y luego, si hay HTTPS, un handshake TLS adicional.

Ese proceso, aunque breve, añade latencia innecesaria y carga al sistema. En servicios con muchas llamadas pequeñas o de alta frecuencia, el impacto acumulado es grande.

Al activar keep-alive, el mismo socket puede usarse para múltiples peticiones. No hay que volver a establecer la conexión ni negociar el cifrado. Es una optimización de transporte pura, menos viajes de red, menos CPU, menos overhead.

Ejemplo práctico

En Node.js (Express) basta con asegurarse de que las respuestas incluyen el header y que el servidor no cierre las conexiones:

En el cliente, puedes forzar el mismo comportamiento:

HTTP/1.1 ya activa conexiones persistentes por defecto, pero declararlo explícitamente evita que se desactive por proxies o configuraciones que cierran sockets tras cada petición.

Resultados y observaciones

Usando Apache Benchmark:

La mejora se nota más en entornos con latencia moderada, conexiones HTTPS y tráfico frecuente. En redes locales o con HTTP/2 la diferencia puede ser menor, ya que HTTP/2 y HTTP/3 ya reutilizan conexiones de forma más avanzada (multiplexing y QUIC).

Consideraciones reales

El keep-alive no es magia: hay que configurarlo correctamente.

Si las conexiones permanecen abiertas demasiado tiempo, pueden acumularse y agotar los sockets del servidor. Por eso existen límites como KeepAliveTimeout o MaxKeepAliveRequests.

También hay que asegurarse de que el balanceador de carga o proxy (por ejemplo, Nginx o HAProxy) respete la persistencia. Algunos interrumpen las conexiones si no detectan actividad en unos segundos.

En general, un timeout de 10-15s suele ser un punto de equilibrio entre rendimiento y recursos.

Conclusión

Optimizar una API no siempre significa cambiar el código. A veces, la diferencia está en cómo viaja la información.

Reducir la cantidad de handshakes y reutilizar conexiones puede mejorar el rendimiento un 20% o más, especialmente en sistemas que hacen muchas peticiones pequeñas o clientes que interactúan constantemente con el backend.

Connection: keep-alive no es nuevo, pero sigue siendo una de las optimizaciones más simples y efectivas que puedes aplicar en cualquier API HTTP/1.1.

Una sola línea. Un 20% de mejora. No siempre hace falta más que eso.


Referencias:

Compartir es construir