Jorge Turbica / blog · Blog ← Todos los artículos EN · Read in English
Lenguajes

Go 1.27 reutiliza mejor las conexiones HTTP/1: de 710 a 150 ms en su benchmark

Go 1.27 vacía en segundo plano los cuerpos HTTP/1 sin leer y devuelve la conexión al pool. Afecta a clientes con muchas peticiones seguidas.

Go 1.27 cambia algo que llevaba años en las listas de buenas prácticas: ya no hace falta vaciar a mano el cuerpo de una respuesta HTTP/1 antes de cerrarlo para que la conexión se reutilice. El equipo de Go lo cuenta en su blog (14 de octubre de 2026) con un benchmark que pasa de ~710 ms a ~150 ms.

Qué cambia

En HTTP/1 las respuestas llegan una detrás de otra por la misma conexión, así que el cliente solo puede reutilizarla cuando el cuerpo actual se ha leído entero. Hasta Go 1.26, si el código llamaba a resp.Body.Close() sin haber leído todo, net/http descartaba la conexión y abría otra, pagando de nuevo TCP y TLS. El artículo estima unos 3 RTT con TLS 1.2 (1 de TCP y 2 de TLS); con TLS 1.3 el handshake puede bajar a 1 RTT. HTTP/2 no tiene este problema porque multiplexa streams.

En Go 1.27, net/http.Transport vacía de forma asíncrona el cuerpo HTTP/1 sin leer durante un máximo de 50 ms o 256 KB, y así puede devolver la conexión al pool. Si la respuesta lleva Connection: close no se intenta vaciar, porque sería trabajo perdido.

El benchmark del artículo hace 100 peticiones secuenciales forzando HTTP/1 con GODEBUG=http2client=0. Según los autores (Nicholas Husin y Damien Neil), tarda ~710 ms en Go 1.26 y ~150 ms en Go 1.27, casi 5 veces menos. Es una medición en una sola máquina y contra un solo servidor, sin varianza, así que conviene leerla como orden de magnitud y no como promesa.

Por qué importa en producción

Los clientes que más lo notan son los que hablan HTTP/1 con muchas peticiones seguidas: llamadas a APIs internas detrás de un proxy que no negocia HTTP/2, sondas, scrapers, clientes de servicios que dejan cuerpos sin leer en rutas de error o al hacer HEAD y GET condicionales. En esos casos cada conexión descartada es latencia extra y más sockets en TIME_WAIT; en el servidor de enfrente, más handshakes TLS.

El cambio también limpia código: los helpers del tipo io.Copy(io.Discard, resp.Body) con tope de tamaño y timeout, que muchos proyectos arrastran, dejan de ser necesarios. El artículo recomienda simplemente defer resp.Body.Close().

Dos matices que cuenta el propio texto: el límite de 256 KB existe porque con cuerpos grandes sin leer es más barato abrir una conexión nueva que drenar, y el comportamiento "puede evolucionar en futuras versiones". Tampoco documenta un GODEBUG para desactivarlo, así que si te preocupa un efecto lateral hay que probarlo, no apagarlo. Otro detalle: un json.Decoder que deja sin leer el salto de línea final también rompía la reutilización en casos pequeños.

Qué haría yo

  1. Medir antes de actualizar: en un servicio que haga muchas llamadas HTTP/1, anotar conexiones nuevas por segundo y latencia p99 del cliente (httptrace o las métricas del proxy/LB).
  2. Actualizar a Go 1.27 en un entorno de pruebas y repetir la medición con el mismo tráfico.
  3. Buscar los helpers de "drenar y cerrar" y comprobar si siguen aportando algo. Retirarlos solo si la medición no empeora.
  4. Revisar los puntos donde se cierra el cuerpo sin leerlo (rutas de error, redirecciones) y asegurarse de que siempre hay un Close.
  5. Vigilar CPU y memoria del proceso: el drenado asíncrono mueve trabajo de la ruta de la petición a segundo plano.

Fuentes

Jorge Turbica

Software Engineer y SRE. Escribo sobre los retos reales que me encuentro en producción.

Ver portfolio ↗