Cuando una petición HTTP tarda más de lo esperado, el problema casi nunca está en un solo lugar. Puede ser DNS, conexión TCP, handshake TLS, espera por conexión reutilizable, el servidor remoto, un retry mal configurado o simplemente que tu cliente está haciendo más trabajo del necesario. Si solo miras el tiempo total, estás adivinando.
En Go, net/http te da una base sólida para hacer requests, pero si quieres entender qué pasó de verdad en cada etapa, httptrace es la herramienta que te permite ver el viaje completo. No reemplaza a OpenTelemetry ni a los logs, pero sí te da una radiografía muy útil cuando necesitas responder una pregunta concreta: ¿dónde se fue el tiempo?
Qué problema resuelve httptrace
httptrace existe para enganchar callbacks al ciclo de vida de una request HTTP. En vez de medir solo inicio y fin, puedes observar eventos como resolución DNS, conexión a la red, uso de una conexión reutilizada, inicio del envío de headers y recepción del primer byte. Eso te ayuda a separar latencia de red, latencia del servidor y latencia de tu propio cliente.
La documentación oficial de Go lo explica en la referencia de net/http/httptrace y vale la pena leerla antes de meterlo en producción: https://pkg.go.dev/net/http/httptrace. Si trabajas con clientes HTTP en Go, esta API te da visibilidad sin tener que instrumentar todo a mano.
Un caso real: si tu servicio en Ecuador hace requests a una API en otra región y ves picos de 900ms, con httptrace puedes descubrir que 250ms son DNS, 180ms son conexión nueva y 300ms se van antes del primer byte. Con ese dato ya no estás “optimizando a ciegas”, sino atacando el tramo que más pesa.
Qué puedes medir exactamente
Con httptrace puedes capturar eventos como:
- inicio y fin de DNS
- inicio y fin de conexión TCP
- inicio y fin de TLS
- si la conexión fue reutilizada o salió nueva
- cuándo se escribió la request
- cuándo llegó el primer byte de respuesta
Eso no significa que siempre tendrás todos los eventos. Si la conexión se reutiliza, por ejemplo, no habrá DNS ni dial nuevo. Ese detalle es justamente útil porque te dice si tu http.Client está aprovechando keep-alive o si estás pagando el costo completo en cada request.
Cómo funciona httptrace en Go
La idea es simple: defines un ClientTrace con callbacks, lo adjuntas al contexto de la request y luego ejecutas el request con ese contexto. Cada callback se dispara en el momento correspondiente del ciclo de vida.
La API oficial de net/http y httptrace está documentada aquí: https://pkg.go.dev/net/http y https://pkg.go.dev/net/http/httptrace. Si usas Go 1.20 o superior, la experiencia es la misma para este caso: construyes un request con contexto y registras trazas por request.
Un detalle importante: httptrace no es un logger automático. Tú decides qué hacer con cada evento. Puedes imprimir tiempos relativos, guardar métricas, mandar spans, o simplemente depurar en consola. Eso lo vuelve flexible, pero también te obliga a ser ordenado con el manejo de timestamps.
Los callbacks más útiles
No necesitas usar todos los callbacks desde el día uno. Para depurar latencia, normalmente bastan estos:
DNSStartyDNSDoneConnectStartyConnectDoneTLSHandshakeStartyTLSHandshakeDoneGotConnWroteHeadersGotFirstResponseByte
Con esos eventos puedes reconstruir una línea de tiempo bastante clara. Si además te interesa ver si el cliente reintentó por una conexión rota, puedes complementar con logs del RoundTripper o con métricas de tu propio código.
Ejemplo práctico con tiempos por etapa
Vamos a armar un ejemplo simple que imprime la duración de cada fase. La idea no es hacer un wrapper sofisticado, sino mostrar una base que puedas adaptar a tu cliente real.
package main
import (
"context"
"fmt"
"net/http"
"net/http/httptrace"
"time"
)
func main() {
url := "https://example.com"
var start time.Time
var dnsStart, connectStart, tlsStart, gotConnAt, wroteHeadersAt, firstByteAt time.Time
trace := &httptrace.ClientTrace{
DNSStart: func(info httptrace.DNSStartInfo) {
dnsStart = time.Now()
fmt.Println("DNS start:", info.Host)
},
DNSDone: func(info httptrace.DNSDoneInfo) {
fmt.Printf("DNS done: %v, coalesced=%v\n", time.Since(dnsStart), info.Coalesced)
},
ConnectStart: func(network, addr string) {
connectStart = time.Now()
fmt.Printf("Connect start: %s %s\n", network, addr)
},
ConnectDone: func(network, addr string, err error) {
fmt.Printf("Connect done: %v, err=%v\n", time.Since(connectStart), err)
},
TLSHandshakeStart: func() {
tlsStart = time.Now()
fmt.Println("TLS handshake start")
},
TLSHandshakeDone: func(state tls.ConnectionState, err error) {
fmt.Printf("TLS handshake done: %v, err=%v\n", time.Since(tlsStart), err)
},
GotConn: func(info httptrace.GotConnInfo) {
gotConnAt = time.Now()
fmt.Printf("Got conn: reused=%v, wasIdle=%v, idleTime=%v\n", info.Reused, info.WasIdle, info.IdleTime)
},
WroteHeaders: func() {
wroteHeadersAt = time.Now()
fmt.Printf("Wrote headers after %v\n", time.Since(gotConnAt))
},
GotFirstResponseByte: func() {
firstByteAt = time.Now()
fmt.Printf("First byte after %v\n", time.Since(wroteHeadersAt))
},
}
req, err := http.NewRequestWithContext(httptrace.WithClientTrace(context.Background(), trace), http.MethodGet, url, nil)
if err != nil {
panic(err)
}
client := &http.Client{Timeout: 10 * time.Second}
start = time.Now()
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Printf("Total request time: %v\n", time.Since(start))
fmt.Printf("Time to first byte: %v\n", firstByteAt.Sub(start))
}
Hay un detalle en el ejemplo: para compilarlo tal cual, necesitas importar crypto/tls. Lo dejé fuera del bloque para concentrarnos en la idea, pero en tu código real no lo olvides. También conviene usar time.Time para marcar hitos y luego calcular duraciones relativas, porque es más fácil de leer que restar offsets manualmente.
En la práctica, puedes imprimir algo como esto:
DNS start: example.com
DNS done: 18.4ms, coalesced=false
Connect start: tcp 93.184.216.34:443
Connect done: 42.1ms, err=<nil>
TLS handshake start
TLS handshake done: 61.7ms, err=<nil>
Got conn: reused=false, wasIdle=false, idleTime=0s
Wrote headers after 1.2ms
First byte after 214.8ms
Total request time: 341.5ms
Con esa salida ya puedes ver si el cuello de botella está en la red o en el servidor remoto. Si el tiempo entre WroteHeaders y GotFirstResponseByte es alto, el servidor tarda en responder. Si el problema está antes, probablemente sea cliente o red.
Cómo leer la línea de tiempo
No todos los eventos tienen el mismo peso. Una request lenta con GotConn mostrando reused=true suele indicar que ya evitaste el costo de TCP y TLS, así que el tiempo restante se mueve más hacia el backend. En cambio, si cada request abre conexión nueva, el costo se acumula rápido.
También es común confundir GotFirstResponseByte con “fin de request”. No lo es. Solo marca el momento en que llegó el primer byte. Si la respuesta es grande o el body se consume lentamente, el tiempo total puede seguir creciendo después de ese evento.
Cómo usarlo para detectar latencia y retries
httptrace no te dice explícitamente “hubo retry”, pero sí te ayuda a ver patrones sospechosos. Si una misma operación genera varias conexiones nuevas, o si el tiempo total supera por mucho el tiempo hasta el primer byte, tienes señales para revisar tu lógica de reintentos, timeouts y configuración del transporte.
Un error común es poner retries sin distinguir el tipo de falla. Si reintentas una request que ya hizo ConnectStart y TLSHandshakeStart, cada intento puede multiplicar el costo. En servicios distribuidos, eso se traduce en cascadas de latencia que luego parecen “intermitencia” cuando en realidad son reintentos mal calibrados.
Señales típicas de problema
Usa estas pistas para leer tus trazas:
DNSDonetarda mucho: revisa resolver, cache DNS o problemas de red.ConnectDonetarda mucho: puede haber saturación, firewall, ruta lenta o destino caído.TLSHandshakeDonetarda mucho: revisa certificados, inspección TLS o distancia a la región.GotConnmuestraReused=falsetodo el tiempo: tu pool de conexiones no se está aprovechando.GotFirstResponseBytetarda mucho: el servidor remoto o una dependencia interna está procesando lento.
Si quieres medir retries de forma seria, lo ideal es combinar httptrace con logs estructurados y métricas de tu capa de cliente. httptrace te da la anatomía de un intento; tu aplicación debe decir cuántos intentos hubo y por qué se dispararon.
Buenas prácticas para llevarlo a producción
La primera regla es no convertir httptrace en ruido. Si imprimes cada callback en todos los requests de alto volumen, vas a llenar logs y a encarecer el cliente. Úsalo para muestreo, debugging puntual o para casos donde realmente necesitas visibilidad por request.
La segunda regla es que el transporte importe. httptrace te mostrará si reutilizas conexiones, pero el comportamiento real depende de tu http.Transport. Si creas un http.Client nuevo en cada request, pierdes keep-alive y verás más dials de los necesarios.
Configuración mínima recomendable
Una base razonable para muchos servicios es esta:
transport := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 20,
IdleConnTimeout: 90 * time.Second,
DisableCompression: false,
ForceAttemptHTTP2: true,
}
client := &http.Client{
Transport: transport,
Timeout: 10 * time.Second,
}
No existe un valor universal para todos los casos. Si tu servicio hace muchas requests concurrentes al mismo host, quizá necesites ajustar MaxIdleConnsPerHost. Si llamas a varios proveedores externos desde LatAm, también conviene revisar timeouts por dependencia y no solo un timeout global.
Qué evitar
- crear un
http.Clientpor request - usar timeouts demasiado agresivos sin medir primero
- asumir que un retry “arregla” latencia cuando solo la oculta
- registrar trazas completas en cada request de producción sin muestreo
- interpretar
GotFirstResponseBytecomo fin total de la operación
Con una base así, httptrace te sirve como herramienta de diagnóstico y no como parche temporal. La diferencia es que aprendes algo útil sobre tu sistema en vez de solo apagar un síntoma.
Integrarlo con observabilidad y debugging real
Si ya usas OpenTelemetry, httptrace puede complementar tus spans. OpenTelemetry te da el mapa general de la transacción, mientras que httptrace te da detalle fino de la request HTTP. En incidentes de latencia, esa combinación es bastante útil porque puedes ver el span general y, dentro de él, las etapas concretas del cliente.
También puedes transformar los callbacks en métricas. Por ejemplo, podrías registrar histogramas para DNS, connect, TLS y TTFB por host. Eso te ayuda a comparar proveedores y regiones. Si una API responde bien desde una región y mal desde otra, el problema puede ser red, no aplicación.
Una forma práctica de hacerlo es guardar solo números agregados, no cada evento. Así reduces ruido y mantienes tendencias útiles. Por ejemplo, un histograma de connect_duration_ms por host te dice si el problema es puntual o sistemático.
Flujo recomendado para depurar una request lenta
- Mide el tiempo total con
http.Clienty un timeout claro. - Adjunta
httptracesolo al request sospechoso. - Compara
DNS,Connect,TLSyTTFB. - Revisa si la conexión fue reutilizada.
- Si hay retries, registra cuántos intentos hubo y con qué error falló cada uno.
- Ajusta
Transport, timeouts o estrategia de retry según el cuello de botella real.
Ese flujo te ahorra tiempo porque te obliga a separar síntomas de causas. No es lo mismo “el endpoint tarda 800ms” que “el 70% del tiempo se va antes de conectar”.
Tabla resumen
| Pregunta corta | Respuesta corta |
|---|---|
¿Qué mide httptrace? | Etapas internas de una request HTTP en Go. |
| ¿Sirve para ver DNS? | Sí, con DNSStart y DNSDone. |
| ¿Sirve para saber si reutilizas conexión? | Sí, con GotConn. |
| ¿Sirve para detectar el primer byte? | Sí, con GotFirstResponseByte. |
| ¿Reemplaza a métricas y trazas distribuidas? | No, las complementa. |
| ¿Ayuda con retries? | Sí, porque muestra el costo real de cada intento. |
Cierre práctico
Si hoy solo mides duración total de requests, estás perdiendo información valiosa. httptrace te permite separar el tiempo en piezas concretas y entender si el problema está en tu cliente, en la red o en el servicio remoto. Para depurar latencia, retries y conexiones mal aprovechadas, esa diferencia importa mucho.
La clave es usarlo con intención: en requests críticas, en incidentes, en pruebas de carga o en diagnósticos puntuales. No necesitas instrumentar todo para siempre; necesitas saber cuándo una request está pagando costos que no ves a simple vista.
Si quieres profundizar más, revisa la documentación oficial de net/http y httptrace en Go:
Preguntas frecuentes
¿Qué diferencia hay entre medir tiempo total y usar httptrace?
¿httptrace funciona con cualquier request HTTP en Go?
¿Puedo usar httptrace en producción?
¿httptrace me dice si hubo retries?
¿Qué evento es el más útil para saber si el backend responde lento?
¿Necesito OpenTelemetry para usar httptrace?
¿Qué debo revisar primero si veo muchas conexiones nuevas?
Azirgo
¿Listo para construir tu Producto Digital?
Sitios web, apps móviles, software a medida y soluciones blockchain. Cuéntanos qué tienes en mente y armamos un plan claro contigo.
- Cotización clara en 48 horas
- Equipo en Ecuador, atención en español
- Desde un MVP hasta un producto en producción