Un desarrollador revisa en una terminal los tiempos de una petición HTTP en Go mientras en una pantalla se ven métricas de DNS, conexión y TLS.
Volver al blog

Cómo trazar requests HTTP en Go

Aprende a usar httptrace en Go para medir DNS, conexiones, TLS y tiempos reales de requests HTTP. Guía práctica para depurar latencia, retries y cuellos de botella en servicios distribuidos, pensada para equipos que trabajan con Go en LatAm.

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:

  • DNSStart y DNSDone
  • ConnectStart y ConnectDone
  • TLSHandshakeStart y TLSHandshakeDone
  • GotConn
  • WroteHeaders
  • GotFirstResponseByte

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:

  1. DNSDone tarda mucho: revisa resolver, cache DNS o problemas de red.
  2. ConnectDone tarda mucho: puede haber saturación, firewall, ruta lenta o destino caído.
  3. TLSHandshakeDone tarda mucho: revisa certificados, inspección TLS o distancia a la región.
  4. GotConn muestra Reused=false todo el tiempo: tu pool de conexiones no se está aprovechando.
  5. GotFirstResponseByte tarda 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.Client por 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 GotFirstResponseByte como 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

  1. Mide el tiempo total con http.Client y un timeout claro.
  2. Adjunta httptrace solo al request sospechoso.
  3. Compara DNS, Connect, TLS y TTFB.
  4. Revisa si la conexión fue reutilizada.
  5. Si hay retries, registra cuántos intentos hubo y con qué error falló cada uno.
  6. 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 cortaRespuesta 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?
El tiempo total solo te dice cuánto tardó la request completa. `httptrace` divide ese tiempo en etapas como DNS, conexión, TLS y primer byte, así puedes encontrar el cuello de botella real con mucha más precisión.
¿httptrace funciona con cualquier request HTTP en Go?
Sí, siempre que uses `net/http` y adjuntes el `ClientTrace` al contexto de la request. No depende de un servidor específico, pero sí del transporte y de si la conexión se reutiliza o no.
¿Puedo usar httptrace en producción?
Sí, pero con criterio. Lo normal es usarlo de forma puntual, por muestreo o en requests sospechosos, porque imprimir cada callback en alto volumen puede generar ruido y costo extra.
¿httptrace me dice si hubo retries?
No lo dice de forma explícita. Lo que te muestra es el comportamiento de cada intento, así que debes combinarlo con logs o métricas de tu capa de cliente para saber cuántos retries ocurrieron y por qué.
¿Qué evento es el más útil para saber si el backend responde lento?
`GotFirstResponseByte` suele ser el más útil para ese caso, porque marca cuándo llegó el primer byte de la respuesta. Si ese tramo es alto y la conexión ya estaba reutilizada, el problema suele estar del lado del servidor o de una dependencia interna.
¿Necesito OpenTelemetry para usar httptrace?
No, `httptrace` funciona solo con `net/http`. Aun así, combinarlo con OpenTelemetry te da una vista más completa: trazas de alto nivel y detalle fino del comportamiento HTTP.
¿Qué debo revisar primero si veo muchas conexiones nuevas?
Primero revisa si estás creando un `http.Client` o `http.Transport` por request. Después valida la configuración de keep-alive, `MaxIdleConnsPerHost` y los timeouts, porque ahí suele estar el problema.

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