15 mar 2025·7 min de lectura

Tiempos de espera con context en Go para APIs: de manejadores HTTP a SQL

Los timeouts con context en Go ayudan a pasar deadlines desde handlers HTTP hasta llamadas SQL, evitar peticiones atascadas y mantener los servicios estables bajo carga.

Tiempos de espera con context en Go para APIs: de manejadores HTTP a SQL

Por qué las peticiones se quedan atascadas (y por qué duele bajo carga)

Una petición queda “atascada” cuando espera algo que no responde: una consulta de base de datos lenta, una conexión bloqueada en el pool, un fallo puntual de DNS o un servicio upstream que acepta la llamada pero nunca contesta.

El síntoma parece simple: algunas peticiones tardan una eternidad y detrás de ellas se acumulan más y más. A menudo verás memoria en aumento, número de goroutines creciendo y una cola de conexiones abiertas que nunca parece vaciarse.

Bajo carga, las peticiones atascadas dañan en dos frentes. Mantienen ocupados a los workers y retienen recursos escasos como conexiones a la base de datos y locks. Eso hace que peticiones normalmente rápidas se vuelvan lentas, lo que genera más solapamiento y aún más esperas.

Los reintentos y los picos de tráfico empeoran esta espiral. Un cliente agota su tiempo y reintenta mientras la petición original sigue en ejecución, así que ahora pagas por dos peticiones. Multiplica eso por muchos clientes durante una breve desaceleración y puedes sobrecargar la base de datos o alcanzar límites de conexiones incluso si el tráfico medio está bien.

Un timeout es simplemente una promesa: “no esperaremos más de X”. Te ayuda a fallar rápido y liberar recursos, pero no hace que el trabajo termine antes.

Tampoco garantiza que el trabajo se detenga instantáneamente. Por ejemplo, la base de datos puede seguir ejecutando la consulta, un servicio upstream puede ignorar tu cancelación o tu propio código puede no ser seguro ante la cancelación.

Lo que sí garantiza un timeout es que tu handler puede dejar de esperar, devolver un error claro y liberar lo que está reteniendo. Esa espera acotada es lo que impide que unas pocas llamadas lentas se conviertan en una caída completa.

El objetivo con los timeouts de context en Go es tener un único deadline compartido desde el borde hasta la llamada más profunda. Fíjalo una vez en el límite HTTP, pasa el mismo contexto por todo el código del servicio y úsalo en llamadas de database/sql para que la base de datos también sepa cuándo dejar de esperar.

Context en Go en términos sencillos

Un context.Context es un objeto pequeño que pasas por tu código para describir qué está pasando ahora mismo. Responde preguntas como: “¿sigue siendo válida esta petición?”, “¿cuándo debemos rendirnos?” y “¿qué valores de ámbito de petición deben viajar con este trabajo?”.

La gran ventaja es que una decisión en el borde del sistema (tu handler HTTP) puede proteger cada paso downstream, siempre que sigas pasando el mismo contexto.

Qué lleva el context

Context no es un lugar para datos de negocio. Es para señales de control y una pequeña cantidad de alcance por petición: cancelación, un deadline/timeout y metadatos pequeños como un request ID para los logs.

Timeout frente a cancelación es simple: un timeout es una razón para cancelar. Si pones un timeout de 2 segundos, el contexto se cancelará cuando pasen 2 segundos. Pero un contexto también puede cancelarse antes si el usuario cierra la pestaña, el balanceador corta la conexión o tu código decide que la petición debe terminar.

El contexto fluye por las llamadas de función como un parámetro explícito, normalmente el primero: func DoThing(ctx context.Context, ...). Ese es el punto. Es difícil “olvidarlo” cuando aparece en cada sitio de llamada.

Cuando el deadline expira, todo lo que observe ese contexto debería detenerse rápidamente. Por ejemplo, una consulta a la base de datos usando QueryContext debería devolver temprano con un error como context deadline exceeded, y tu handler puede responder con un timeout en lugar de colgarse hasta que el servidor se quede sin workers.

Un buen modelo mental: una petición, un contexto, pasado por todas partes. Si la petición muere, el trabajo también debería hacerlo.

Fijar un deadline claro en el borde HTTP

Si quieres que los timeouts de extremo a extremo funcionen, decide dónde empieza el reloj. El lugar más seguro es justo en el borde HTTP, de modo que cada llamada downstream (lógica de negocio, SQL, otros servicios) herede el mismo deadline.

Puedes fijar ese deadline en varios sitios. Los timeouts a nivel de servidor son una buena base y te protegen de clientes lentos. El middleware es ideal para consistencia entre grupos de rutas. Fijarlo dentro del handler también está bien cuando quieres algo explícito y local.

Para la mayoría de APIs, los timeouts por petición en middleware o en el handler son los más fáciles de razonar. Manténlos realistas: los usuarios prefieren un fallo rápido y claro a una petición que se queda colgada. Muchos equipos usan presupuestos más cortos para lecturas (1–2s) y algo más largos para escrituras (3–10s), según lo que haga el endpoint.

Aquí tienes un patrón simple de handler:

func (s *Server) getReport(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
    defer cancel()

    report, err := s.reports.Generate(ctx, r.URL.Query().Get("id"))
    if err != nil {
        http.Error(w, err.Error(), http.StatusGatewayTimeout)
        return
    }

    json.NewEncoder(w).Encode(report)
}

Dos reglas mantienen esto efectivo:

  • Siempre llama a cancel() para que timers y recursos se liberen rápido.
  • Nunca reemplaces el contexto de la petición con context.Background() o context.TODO() dentro del handler. Eso rompe la cadena, y tus llamadas a la base de datos y peticiones salientes pueden ejecutarse para siempre incluso después de que el cliente se haya ido.

Propagar el context por tu base de código

Una vez que fijas un deadline en el borde HTTP, el trabajo real es asegurarte de que ese mismo deadline alcance cada capa que pueda bloquear. La idea es un reloj compartido entre el handler, el código del servicio y cualquier cosa que toque la red o el disco.

Una regla simple mantiene las cosas consistentes: cada función que pueda esperar debería aceptar un context.Context, y debería ser el primer parámetro. Eso lo hace obvio en los sitios de llamada y se vuelve hábito.

Un patrón de firmas práctico

Prefiere firmas como DoThing(ctx context.Context, ...) para servicios y repositorios. Evita esconder context dentro de structs o recrearlo con context.Background() en capas inferiores, porque eso silenciosamente descarta el deadline del llamador.

func (h *Handler) CreateOrder(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    if err := h.svc.CreateOrder(ctx, r.Body); err != nil {
        // map context errors to a clear client response elsewhere
        http.Error(w, err.Error(), http.StatusRequestTimeout)
        return
    }
}

func (s *Service) CreateOrder(ctx context.Context, body io.Reader) error {
    // parsing or validation can still respect cancellation
    select {
    case <-ctx.Done():
        return ctx.Err()
    default:
    }

    return s.repo.InsertOrder(ctx, /* data */)
}

Manejar salidas tempranas con claridad

Trata a ctx.Done() como una vía de control normal. Dos hábitos ayudan:

  • Comprueba ctx.Err() antes de empezar trabajo costoso y después de bucles largos.
  • Devuelve ctx.Err() hacia arriba sin cambiarlo, para que el handler pueda responder rápido y dejar de desperdiciar recursos.

Cuando cada capa pasa el mismo ctx, un único timeout puede cortar el parsing, la lógica de negocio y las esperas en la base de datos de una sola vez.

Aplicar deadlines a consultas de database/sql

Convierte ideas de observabilidad en herramientas
Usa AppMaster para crear portales internos para el seguimiento de latencia y timeouts.
Crear portal

Una vez que tu handler HTTP tiene un deadline, asegúrate de que el trabajo en la base de datos realmente lo escuche. Con database/sql, eso significa usar los métodos con contexto cada vez. Si llamas a Query() o Exec() sin contexto, tu API puede seguir esperando una consulta lenta incluso después de que el cliente haya desistido.

Usa esto de forma consistente: db.QueryContext, db.QueryRowContext, db.ExecContext y db.PrepareContext (luego QueryContext/ExecContext en la statement retornada).

func (s *Store) GetUser(ctx context.Context, id int64) (*User, error) {
	row := s.db.QueryRowContext(ctx,
		`SELECT id, email FROM users WHERE id = $1`, id,
	)
	var u User
	if err := row.Scan(&u.ID, &u.Email); err != nil {
		return nil, err
	}
	return &u, nil
}

func (s *Store) UpdateEmail(ctx context.Context, id int64, email string) error {
	_, err := s.db.ExecContext(ctx,
		`UPDATE users SET email = $1 WHERE id = $2`, email, id,
	)
	return err
}

Dos cosas son fáciles de pasar por alto.

Primero, tu driver SQL debe respetar la cancelación del contexto. Muchos lo hacen, pero confírmalo en tu stack probando una consulta deliberadamente lenta y comprobando que se cancela rápido cuando vence el deadline.

Segundo, considera un timeout del lado de la base de datos como respaldo. Por ejemplo, Postgres puede aplicar un límite por sentencia (statement timeout). Eso protege la base de datos incluso si un bug en la app olvida pasar el contexto en algún sitio.

Cuando una operación se detiene por timeout, trátala distinto a un error SQL normal. Comprueba errors.Is(err, context.DeadlineExceeded) y errors.Is(err, context.Canceled) y devuelve una respuesta clara (como un 504) en lugar de tratarlo como “la base de datos está rota”. Si generas backends Go (por ejemplo con AppMaster), mantener estas rutas de error separadas también facilita razonar sobre logs y reintentos.

Llamadas downstream: clientes HTTP, caches y otros servicios

Aunque tu handler y tus consultas SQL respeten el contexto, una petición aún puede colgarse si una llamada downstream espera para siempre. Bajo carga, unas pocas goroutines atascadas pueden acumularse, agotar pools de conexión y convertir una pequeña desaceleración en una caída completa. La solución es propagar de forma consistente más un tope duro.

HTTP saliente

Al llamar a otra API, construye la petición con el mismo contexto para que el deadline y la cancelación fluyan automáticamente.

req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil { /* handle */ }
resp, err := httpClient.Do(req)

No te fíes solo del contexto. También configura el cliente HTTP y el transporte para protegerte si el código usa por error un background context, o si DNS/TLS/conexiones inactivas se quedan estancadas. Ajusta http.Client.Timeout como límite máximo para toda la llamada, configura timeouts del transporte (dial, TLS handshake, response header) y reutiliza un único cliente en lugar de crear uno nuevo por petición.

Caches y colas

Caches, brokers de mensajes y clientes RPC a menudo tienen sus propios puntos de espera: adquirir una conexión, esperar una respuesta, bloquearse en una cola llena o esperar un lock. Asegúrate de que esas operaciones acepten ctx y también usa timeouts a nivel de librería cuando estén disponibles.

Una regla práctica: si la petición del usuario tiene 800ms restantes, no inicies una llamada downstream que pueda tardar 2 segundos. Omítela, degrada la respuesta o devuelve datos parciales.

Decide de antemano qué significa un timeout para tu API. A veces la respuesta correcta es un error rápido. A veces es datos parciales para campos opcionales. A veces es datos en caché claramente marcados.

Si construyes backends Go (incluidos los generados, como en AppMaster), esto es la diferencia entre “existen timeouts” y “los timeouts protegen consistentemente el sistema” cuando el tráfico sube.

Paso a paso: refactorizar una API para usar timeouts de extremo a extremo

Parte desde una plantilla funcional
Inicia desde una plantilla funcional y itera rápido a medida que cambian los requisitos.
Empezar

Refactorizar para timeouts se reduce a un hábito: pasar el mismo context.Context desde el borde HTTP hasta cada llamada que pueda bloquear.

Una forma práctica de hacerlo es trabajar de arriba hacia abajo:

  • Cambia tus handlers y métodos centrales del servicio para aceptar ctx context.Context.
  • Actualiza cada llamada a BD para usar QueryContext o ExecContext.
  • Haz lo mismo para llamadas externas (HTTP, caches, colas). Si una librería no acepta ctx, envuélvela o reemplázala.
  • Decide quién es dueño de los timeouts. Una regla común: el handler fija el deadline global; las capas inferiores solo fijan deadlines más cortos para operaciones específicas cuando hace falta.
  • Haz que los errores sean previsibles en el borde: mapea context.DeadlineExceeded y context.Canceled a respuestas HTTP claras.

Aquí tienes la forma que quieres a través de las capas:

func (h *Handler) GetOrder(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
    defer cancel()

    order, err := h.svc.GetOrder(ctx, r.PathValue("id"))
    if errors.Is(err, context.DeadlineExceeded) {
        http.Error(w, "request timed out", http.StatusGatewayTimeout)
        return
    }
    if err != nil {
        http.Error(w, "internal error", http.StatusInternalServerError)
        return
    }
    _ = json.NewEncoder(w).Encode(order)
}

func (r *Repo) GetOrder(ctx context.Context, id string) (Order, error) {
    row := r.db.QueryRowContext(ctx, `SELECT id,total FROM orders WHERE id=$1`, id)
    // scan...
}

Los valores de timeout deberían ser aburridos y consistentes. Si el handler tiene 2 segundos en total, mantén las consultas a la BD por debajo de 1 segundo para dejar margen para la codificación JSON y otro trabajo.

Para demostrar que funciona, añade una prueba que fuerce un timeout. Un enfoque sencillo es un repositorio falso cuyo método bloquea hasta ctx.Done() y luego devuelve ctx.Err(). Tu prueba debería afirmar que el handler devuelve un 504 rápido, no después del retraso falso.

Si generas backends Go con un generador (por ejemplo, AppMaster genera servicios Go), la regla es la misma: un contexto por petición, hiloado en todas partes, con una propiedad clara del deadline.

Observabilidad: demostrar que los timeouts funcionan

Despliega donde corre tu stack
Lanza apps en AppMaster Cloud o en tu propio AWS, Azure o Google Cloud.
Desplegar App

Los timeouts solo ayudan si puedes ver que están ocurriendo. El objetivo es simple: cada petición tiene un deadline y cuando falla puedes saber dónde se fue el tiempo.

Empieza con logs que sean útiles y seguros. En vez de volcar cuerpos completos, registra lo suficiente para conectar las piezas y detectar caminos lentos: request ID (o trace ID), si hay un deadline y cuánto tiempo queda en puntos clave, el nombre de la operación (handler, nombre de consulta SQL, nombre de llamada externa) y la categoría del resultado (ok, timeout, canceled, otro error).

Añade unas métricas enfocadas para que el comportamiento bajo carga sea evidente:

  • Conteo de timeouts por endpoint y dependencia
  • Latencia de petición (p50/p95/p99)
  • Peticiones en vuelo
  • Latencia de consultas BD (p95/p99)
  • Tasa de errores dividida por tipo

Cuando gestiones errores, etiquétalos correctamente. context.DeadlineExceeded suele significar que alcanzaste tu presupuesto. context.Canceled a menudo significa que el cliente se fue o que un timeout upstream se disparó primero. Manténlos separados porque las soluciones son distintas.

Tracing: encontrar dónde se va el tiempo

Los spans de tracing deberían seguir el mismo contexto desde el handler HTTP hasta las llamadas database/sql como QueryContext. Por ejemplo, una petición expira a los 2 segundos y el trace muestra 1.8 segundos esperando una conexión a la BD. Eso apunta al tamaño del pool o a transacciones lentas, no al texto de la consulta.

Si construyes un dashboard interno para esto (timeouts por ruta, consultas lentas), una herramienta sin código como AppMaster puede ayudarte a desplegarlo rápido sin convertir la observabilidad en un proyecto aparte.

Errores comunes que anulan tus timeouts

La mayoría de los bugs de “aún se cuelga a veces” vienen de unos pocos errores pequeños.

  • Resetear el reloj en mitad del vuelo. Un handler fija 2s de deadline, pero el repositorio crea un contexto nuevo con su propio timeout (o sin timeout). Ahora la base de datos puede seguir ejecutando después de que el cliente se fue. Pasa el ctx entrante y solo ajústalo cuando tengas una razón clara.
  • Iniciar goroutines que nunca paran. Lanzar trabajo con context.Background() (o descartar el ctx por completo) hace que siga ejecutándose aun después de la cancelación de la petición. Pasa el ctx de la petición a las goroutines y select sobre ctx.Done().
  • Deadlines demasiado cortos para tráfico real. Un timeout de 50ms puede funcionar en tu portátil y fallar en producción durante un pequeño pico, provocando reintentos, más carga y una mini-caída que te causas a ti mismo. Elige timeouts basados en latencia normal más margen.
  • Ocultar el error real. Tratar context.DeadlineExceeded como un 500 genérico empeora la depuración y el comportamiento del cliente. Mapea a una respuesta de timeout clara y registra la diferencia entre “cancelado por cliente” y “tiempo agotado”.
  • Dejar recursos abiertos en salidas tempranas. Si devuelves temprano, asegúrate de defer rows.Close() y de llamar al cancel devuelto por context.WithTimeout. Filas filtradas o trabajo rezagado pueden agotar conexiones bajo carga.

Un ejemplo rápido: un endpoint dispara una consulta de reporte. Si el usuario cierra la pestaña, el ctx del handler se cancela. Si tu llamada SQL usó un contexto background nuevo, la consulta sigue ejecutándose, ocupando una conexión y ralentizando a todos. Cuando propagas el mismo ctx a QueryContext, la llamada a la base de datos se interrumpe y el sistema se recupera más rápido.

Lista rápida para comportamientos fiables con timeouts

Crea apps móviles con tu API
Crea apps nativas iOS y Android que hablen con tu backend con timeouts predecibles.
Construir móvil

Los timeouts solo ayudan si son consistentes. Una sola llamada perdida puede mantener una goroutine ocupada, retener una conexión a BD y ralentizar las siguientes peticiones.

  • Fija un deadline claro en el borde (por lo general el handler HTTP). Todo dentro de la petición debe heredarlo.
  • Pasa el mismo ctx por las capas de servicio y repositorio. Evita context.Background() en código de petición.
  • Usa métodos de BD con contexto en todas partes: QueryContext, QueryRowContext y ExecContext.
  • Adjunta el mismo ctx a llamadas salientes (HTTP, caches, colas). Si creas un contexto hijo, hazlo más corto, no más largo.
  • Maneja cancelaciones y timeouts de forma coherente: devuelve un error claro, detén el trabajo y evita bucles de reintentos dentro de una petición cancelada.

Después de eso, verifica el comportamiento bajo presión. Un timeout que se dispara pero no libera recursos con suficiente rapidez sigue dañando la fiabilidad.

Los dashboards deberían dejar los timeouts evidentes, no ocultos en promedios. Controla señales que respondan a “¿se aplican realmente los deadlines?”: timeouts de petición y de BD (separados), percentiles de latencia (p95, p99), stats del pool BD (conexiones en uso, conteo de espera, duración de espera) y un desglose de causas de error (context deadline exceeded vs otros fallos).

Si construyes herramientas internas en una plataforma como AppMaster, la misma lista aplica a cualquier servicio Go que conectes: define deadlines en el borde, propágalos y confirma en métricas que las peticiones atascadas pasan a fallos rápidos en lugar de montones de espera.

Escenario de ejemplo y siguientes pasos

Un lugar común donde esto rinde es un endpoint de búsqueda. Imagina GET /search?q=printer se enlentece cuando la base de datos está ocupada con una consulta grande de reporte. Sin un deadline, cada petición entrante puede quedarse esperando una consulta larga. Bajo carga, esas peticiones atascadas se acumulan, ocupan goroutines y conexiones, y la API entera se siente congelada.

Con un deadline claro en el handler HTTP y el mismo ctx pasado al repositorio, el sistema deja de esperar cuando se gasta el presupuesto. Cuando vence el deadline, el driver de la BD cancela la consulta (cuando lo soporta), el handler responde y el servidor puede seguir atendiendo nuevas peticiones en lugar de esperar para siempre.

El comportamiento visible para el usuario es mejor incluso cuando algo falla. En vez de quedarse girando 30–120 segundos y luego fallar de forma caótica, el cliente recibe un error rápido y predecible (a menudo 504 o 503 con un mensaje corto como “request timed out”). Más importante, el sistema se recupera rápido porque las peticiones nuevas no quedan bloqueadas detrás de las antiguas.

Siguientes pasos para consolidarlo entre endpoints y equipos:

  • Elige timeouts estándar por tipo de endpoint (search vs writes vs exports).
  • Requiere QueryContext y ExecContext en las revisiones de código.
  • Haz que los errores de timeout sean explícitos en el borde (código de estado claro, mensaje simple).
  • Añade métricas para timeouts y cancelaciones para notar regresiones temprano.
  • Escribe un helper que envuelva la creación de context y logging para que cada handler se comporte igual.

Si estás construyendo servicios y herramientas internas con AppMaster, puedes aplicar estas reglas de timeout de forma consistente a backends Go generados, integraciones de API y dashboards en un solo lugar. AppMaster está disponible en appmaster.io (no-code, con generación real de código Go), así que puede encajar cuando quieras un manejo consistente de peticiones y observabilidad sin construir cada herramienta administrativa a mano.

FAQ

¿Qué significa que una petición esté “atascada” en una API Go?

Una petición está “atascada” cuando espera algo que no responde, como una consulta SQL lenta, una conexión bloqueada en el pool, problemas de DNS o un servicio upstream que no devuelve respuesta. Bajo carga, las peticiones atascadas se acumulan, ocupan trabajadores y conexiones, y pueden convertir una pequeña desaceleración en una caída más amplia.

¿Dónde debo establecer el timeout: middleware, handler o más profundo en el código?

Fija el deadline general en el borde HTTP y pasa ese mismo ctx a cada capa que pueda bloquearse. Ese deadline compartido evita que unas pocas operaciones lentas retengan recursos el tiempo suficiente como para que todo se degrade.

¿Por qué necesito llamar a `cancel()` si el timeout se disparará de todas formas?

Usa ctx, cancel := context.WithTimeout(r.Context(), d) y siempre defer cancel() en el handler (o middleware). Llamar a cancel libera timers y ayuda a detener la espera de forma inmediata cuando la petición termina antes del plazo.

¿Cuál es el error más grande que hace que los timeouts sean inútiles?

No sustituyas el contexto por context.Background() o context.TODO() en código de petición, porque eso rompe la cancelación y los deadlines. Si descartas el contexto de la petición, trabajos downstream como SQL u HTTP saliente pueden seguir ejecutándose incluso después de que el cliente se haya ido.

¿Cómo debo manejar `context deadline exceeded` frente a `context canceled`?

Trata context.DeadlineExceeded y context.Canceled como resultados normales de control y propágales hacia arriba sin modificarlos. En el borde, mapea esos casos a respuestas claras (a menudo 504 para timeouts) para que los clientes no reintenten ciegamente ante un 500 genérico.

¿Qué llamadas de `database/sql` deberían usar context?

Usa los métodos con contexto en todas partes: QueryContext, QueryRowContext, ExecContext y PrepareContext. Si llamas a Query() o Exec() sin contexto, tu handler puede expirar pero la llamada a la base de datos puede quedarse bloqueando la goroutine y reteniendo una conexión.

¿Cancelar un contexto realmente detiene una consulta PostgreSQL en ejecución?

Muchos drivers sí cancelan la consulta en la base de datos, pero debes verificarlo en tu propio stack ejecutando una consulta deliberadamente lenta y comprobando que se cancela rápido cuando vence el deadline. También es prudente usar un timeout del lado de la BD (por ejemplo, statement timeout en Postgres) como respaldo si algún camino olvida pasar ctx.

¿Cómo aplico el mismo deadline a llamadas HTTP salientes?

Crea la petición saliente con http.NewRequestWithContext(ctx, ...) para que el mismo deadline y la cancelación fluyan automáticamente. Además, configura timeouts en el cliente y en el transporte HTTP como límite superior, ya que el contexto no te protege si alguien usa por error un background context o si hay bloqueos a nivel de DNS/TLS/conexiones.

¿Deberían las capas inferiores (repo/servicios) crear sus propios timeouts?

Evita crear contextos nuevos que extiendan el presupuesto de tiempo en capas profundas; los contextos hijos deben ser más cortos, no más largos. Si la petición tiene poco tiempo restante, omite llamadas downstream opcionales, devuelve datos parciales cuando tenga sentido o falla rápido con un error claro.

¿Qué debo monitorizar para demostrar que los timeouts de extremo a extremo funcionan?

Mide los timeouts y las cancelaciones por separado por endpoint y dependencia, junto con percentiles de latencia y peticiones en vuelo. En traces, sigue el mismo contexto desde el handler hasta las llamadas QueryContext para ver si el tiempo se consumió esperando una conexión a la BD, ejecutando una consulta o bloqueado en otro servicio.

Fácil de empezar
Crea algo sorprendente

Experimente con AppMaster con plan gratuito.
Cuando esté listo, puede elegir la suscripción adecuada.

Empieza