Módulo 3: Métricas — latencia, throughput y errores

2. Latencia: qué es y por qué percentiles, no promedio

Descripción

La latencia es el primer instrumento del tablero y el más importante, porque es el que el usuario siente. Nadie percibe directamente cuántas peticiones por segundo procesa tu API; lo que percibe es que cuando hizo clic en Cotizar, el precio apareció en 40 milisegundos o en 4 segundos. Esa espera —el tiempo entre que la petición sale y la respuesta llega— es la latencia, y esta lección la abre por completo: qué mide exactamente, cómo se descompone por dentro, la diferencia crucial entre la latencia que ve el cliente y la que procesa el servidor, y —la lección que atraviesa todo el módulo— por qué la latencia jamás se resume con el promedio.

Empezamos por lo concreto. En k6, la latencia de una petición HTTP se llama http_req_duration, y no es un número atómico: es la suma de tres tramos (http_req_sending + http_req_waiting + http_req_receiving), donde el tramo central, http_req_waiting, es el famoso TTFB (time to first byte, el tiempo que el servidor tarda en empezar a responder). Entender esa anatomía te deja diagnosticar dónde se va el tiempo. Luego medimos de verdad, con el generador de Python, algo que separa a quien entiende latencia de quien la recita: que la latencia que observa el cliente no es la misma que el cómputo del servidor, porque el cliente incluye la espera en cola. Y con esos números en la mano, das el primer golpe a la trampa del promedio.

Conexión con el módulo: esta lección instala el concepto de latencia y la intuición de por qué el promedio falla; la lección 3 hace la parte mecánica —calcular p50/p90/p95/p99 con statistics.quantiles—; y la lección 7 es el clímax, donde vemos con toda la fuerza numérica cuánto miente el promedio sobre una distribución con cola. Aquí reusamos el generador de carga y los VUs del módulo 2 sin reexplicarlos: si necesitas refrescar qué es un VU, vuelve un momento al módulo 2. Lo nuevo aquí es qué mides con esos VUs.

El tiempo de espera del ascensor

Imagina que administras un edificio de oficinas y quieres saber si el ascensor "va bien". Mides el tiempo de espera de cada persona que lo llama: desde que aprieta el botón hasta que las puertas se abren. Esa es la latencia del ascensor. Ahora bien, ese tiempo no es una sola cosa por dentro: hay un tramo en que el sistema registra tu llamada (envío), un tramo grande en que la cabina viaja hasta tu piso (el tiempo de trabajo, el que de verdad importa) y un tramo en que las puertas se abren (recepción). Si el ascensor tarda, saber en cuál de los tres tramos se fue el tiempo te dice si el problema es el motor o las puertas. Eso es exactamente la anatomía de http_req_duration: envío + trabajo del servidor + recepción.

Y aquí viene el matiz que casi todos pasan por alto. Cuando tú solo llamas al ascensor a las tres de la mañana, el edificio vacío, la cabina llega en diez segundos: ese es el tiempo de servicio puro —lo que tarda el mecanismo cuando nadie más lo usa—. Pero a las nueve de la mañana, con doscientas personas llamándolo en todos los pisos, tu espera no son diez segundos: son dos minutos, porque la cabina está ocupada atendiendo a otros y tú haces cola. El mecanismo no se volvió más lento —la cabina sube igual de rápido—; lo que pasó es que ahora esperas tu turno. Esa distinción —tiempo de servicio (el trabajo puro) vs tiempo que ve el usuario (trabajo + cola)— es la diferencia entre la latencia del servidor y la latencia del cliente, y la vas a ver medida en esta lección.

Por último, el error del administrador ingenuo: reportar "el ascensor tarda de media 25 segundos". El promedio suena tranquilizador, pero esconde que a las tres de la mañana tarda 10 segundos y a las nueve tarda dos minutos. La persona que espera dos minutos no vive un "promedio de 25 segundos"; vive dos minutos, y está furiosa. El promedio es una mentira estadística cómoda: real como número, falsa como descripción de lo que la gente experimenta. Por eso, para latencias, miramos percentiles.

http_req_duration: la anatomía de una latencia

En k6, la métrica de latencia de una petición HTTP es http_req_duration. Es la que más vas a mirar en toda la guía, y conviene saber que no es indivisible. Según la documentación oficial de k6, se descompone así:

http_req_duration = http_req_sending + http_req_waiting + http_req_receiving
TramoQué mideAnalogía del ascensor
http_req_sendingTiempo enviando los datos de la petición al servidor.Registrar tu llamada (apretar el botón).
http_req_waitingTiempo esperando la respuesta del servidor: el TTFB (time to first byte). Es el trabajo real del servidor.La cabina viajando hasta tu piso.
http_req_receivingTiempo recibiendo los datos de la respuesta.Las puertas abriéndose.

(k6 mide además dos tramos previos a la petición —http_req_blocked, esperar un hueco de conexión TCP, y http_req_connecting, establecer la conexión— pero esos no cuentan dentro de http_req_duration.) De los tres tramos de la duración, el que casi siempre domina y el que de verdad refleja la salud del backend es http_req_waiting, el TTFB: es el tiempo que el servidor tardó en procesar la petición y empezar a responder. Si tu latencia total es alta y el TTFB es la mayor parte, el cuello de botella está en el servidor (o su base de datos); si el TTFB es bajo pero receiving es alto, el problema es transferir una respuesta enorme o una red lenta. Diagnosticar dónde se va el tiempo es lo que esta anatomía te regala.

Ejemplo trabajado: la latencia del cliente no es la del servidor

Vamos a medir de verdad la distinción más importante de la lección: que la latencia que observa el cliente (lo que mide el generador, e incluye la espera en cola) puede ser decenas de veces mayor que el cómputo del servidor (el trabajo puro). Usamos el /quote normal de Reservo —el canónico, sin trucos— y lo medimos de dos maneras: primero con un solo cliente (sin cola: se aproxima al tiempo de servicio puro) y luego con 60 clientes concurrentes (con cola: la latencia que vive un usuario cuando hay tráfico).

El generador es el mismo del módulo 1, que cronometra lado cliente: arranca el reloj justo antes de mandar la petición y lo para justo después de recibir la respuesta completa. Ese time.perf_counter() de punta a punta es, por definición, la latencia del cliente —incluye todo: la cola, el transporte por localhost y el cómputo del servidor—.

import json, statistics, sys, time, urllib.request
from concurrent.futures import ThreadPoolExecutor

PORT = int(sys.argv[1])
URL = f"http://127.0.0.1:{PORT}/quote"
PAYLOAD = json.dumps({"room": "Focus", "tier": "basic", "hours": 3}).encode()

def one(_):
    """Una petición. Devuelve la latencia en ms, cronometrada lado CLIENTE."""
    start = time.perf_counter()
    req = urllib.request.Request(URL, data=PAYLOAD,
                                 headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=10) as resp:
        resp.read()
    return (time.perf_counter() - start) * 1000

def q(data, p):
    return statistics.quantiles(data, n=100, method="inclusive")[p - 1]

# 1) casi "puro servidor": 1 cliente, sin contención
solo = sorted(one(0) for _ in range(300))
# 2) bajo carga: 60 clientes concurrentes
with ThreadPoolExecutor(max_workers=60) as pool:
    carga = sorted(pool.map(one, range(2000)))

print(f"1 cliente   (~servicio): p50 {q(solo, 50):6.3f} ms   p95 {q(solo, 95):6.3f} ms")
print(f"60 clientes (cliente)  : p50 {q(carga, 50):6.3f} ms   p95 {q(carga, 95):6.3f} ms")

Qué esperar. Con un solo cliente, la latencia es sub-milisegundo: Reservo calcula 2500 * 3 = 7500 en un abrir y cerrar de ojos. Con 60 clientes a la vez, la misma API, el mismo cálculo, tarda 30-50 veces más —y esa diferencia no es cómputo del servidor, es espera en cola—. Esta es la salida real:

1 cliente   (~servicio): p50  0.229 ms   p95  0.330 ms
60 clientes (cliente)  : p50 10.184 ms   p95 18.376 ms

Léelo despacio, porque es una de las verdades centrales del performance testing. El servidor no se volvió lento: procesar una cotización sigue costándole fracciones de milisegundo (los 0.229 ms del caso sin contención lo confirman). Lo que cambió es que, con 60 clientes compitiendo, cada petición hace fila antes de ser atendida. La latencia del cliente —10 ms de mediana, 18 ms de p95— es trabajo del servidor + espera en cola, y bajo carga la cola es la parte gorda. Por eso medir una petición aislada con curl te dice el tiempo de servicio, no la latencia bajo carga: son propiedades distintas del mismo sistema, y la que le importa al usuario es la segunda.

En términos de k6: http_req_waiting (el TTFB) captura sobre todo el trabajo del servidor, pero http_req_duration completo —y desde luego lo que el usuario siente— incluye la espera. Cuando en producción veas un p95 alto con un TTFB también alto, el servidor está saturado; cuando veas un p95 alto con un TTFB bajo, la petición pasó tiempo esperando (en cola, en conexión) antes de que el servidor la tocara.

Por qué el promedio miente: el primer vistazo

Ya tienes la intuición del ascensor; ahora el primer dato duro. Tomemos una corrida del /quote normal con 30 clientes concurrentes y miremos, lado a lado, el promedio y los percentiles. Esta es la salida real del generador (endpoint /quote, 3000 peticiones, 30 clientes):

throughput RPS     5069.7 req/s
errors           0 (0.00%)
-- latencia (ms), lado cliente --
min                  0.94
avg (promedio)       5.88
median (p50)         5.44
p90                  8.45
p95                  9.43
p99                 14.55
max                 34.54

Aquí el promedio (5.88 ms) y la mediana (5.44 ms) casi coinciden, y el p95 (9.43 ms) no está lejos. ¿Por qué? Porque /quote es un endpoint sano, sin cola larga: casi todas las peticiones tardan parecido, así que la distribución es compacta y el promedio la describe bien. Guarda esta corrida como el caso feliz: cuando el promedio y el p95 están cerca, no hay cola que temer.

El problema aparece cuando la distribución tiene cola —unas pocas peticiones mucho más lentas que el resto—, que es lo normal en producción (un pico de la base de datos, un garbage collector que se dispara, un lock). En ese escenario el promedio y el p95 se divorcian, y el promedio empieza a mentir. Para verlo, este módulo usa /quote_slow, el endpoint lento declarado. Esta es su salida real (2000 peticiones, 50 clientes):

avg (promedio)      28.64
median (p50)        12.18
p90                 38.59
p95                182.81
p99                240.31
max                257.33

Mira lo que pasó. El promedio dice 28.64 ms. Pero la mediana —el usuario típico— vio 12.18 ms, menos de la mitad del promedio. Y el p95 —el 5% peor— vio 182.81 ms, seis veces el promedio. Ningún usuario real vivió "28.64 ms": la mayoría vivió ~12 ms y una minoría sufrió ~180 ms. El promedio es el punto donde nadie está: cae en la tierra de nadie entre el grueso rápido y la cola lenta, arrastrado hacia arriba por unos pocos valores enormes. Reportar "la API tarda 28.64 ms de media" describe a un usuario que no existe. Esto es por qué miramos percentiles, y la lección 3 te enseña a calcularlos con precisión. Por ahora quédate con la frase:

El promedio de una latencia con cola cae donde no está casi nadie: por encima del usuario típico (el p50) y muy por debajo del que sufre (el p95). Una latencia se describe con percentiles —p50 para el típico, p95/p99 para la cola—, nunca con un solo promedio.

Errores comunes

Medir con curl una vez y llamarlo "la latencia". Qué pasa: alguien hace curl a /quote, ve 2 ms, y reporta "la API responde en 2 ms". Por qué pasa: confunde el tiempo de servicio (una petición aislada, sin cola) con la latencia bajo carga (con cola). Cómo detectarlo: si tu medición no tuvo concurrencia, mediste el reposo, no la carga. Cómo corregirlo: mide con varios clientes a la vez, como el generador de esta lección —la misma API pasó de 0.23 ms con un cliente a 18 ms de p95 con 60—. La latencia que le importa al usuario es la de carga.

Reportar el promedio de latencia. Qué pasa: el reporte dice "latencia media: 28 ms" y todos quedan tranquilos, sin ver que el p95 es 180 ms. Por qué pasa: el promedio es el resumen por defecto de casi todo, y para latencias es justo el equivocado. Cómo detectarlo: si tu número de latencia es un avg y no un percentil, estás describiendo a un usuario que no existe. Cómo corregirlo: reporta al menos p50 y p95 (y p99 si te importa la cola extrema). El promedio, para latencias, se ignora.

Confundir http_req_waiting con http_req_duration. Qué pasa: alguien optimiza el servidor mirando solo el TTFB (waiting) y no entiende por qué el usuario sigue quejándose. Por qué pasa: el TTFB es solo un tramo de la duración total; si el tiempo se va en receiving (respuesta enorme) o en la espera de conexión, bajar el TTFB no ayuda. Cómo detectarlo: si http_req_duration es alto pero http_req_waiting es bajo, el cuello no está en el cómputo del servidor. Cómo corregirlo: mira la descomposición completa —sending, waiting, receiving— y ataca el tramo que domina, no el que asumes.

Ejercicios

Ejercicio 1 — Diagnostica por la anatomía. Para cada caso, di dónde está probablemente el cuello de botella usando la descomposición sending + waiting + receiving. (a) http_req_duration = 900 ms, de los cuales http_req_waiting = 870 ms. (b) http_req_duration = 900 ms, waiting = 40 ms, receiving = 840 ms. (c) http_req_duration = 900 ms, pero http_req_blocked (previo) = 850 ms.

Ver solución
  • (a) El tiempo se va en waiting (TTFB). El servidor tarda en procesar y empezar a responder: el cuello está en el backend (cómputo, base de datos, un servicio del que depende). Es el caso más común.
  • (b) El servidor responde rápido (waiting = 40 ms), pero transferir la respuesta tarda 840 ms (receiving). El cuello está en el tamaño de la respuesta o la red: una respuesta enorme, o un enlace lento. Optimizar el servidor no ayudaría; hay que reducir el payload.
  • (c) El grueso (blocked = 850 ms) es previo a la petición y ni siquiera cuenta dentro de http_req_duration. Es tiempo esperando un hueco de conexión TCP: el cuello es el pool de conexiones (demasiado pocas conexiones para tanta concurrencia), no el servidor.

La lección: "la API está lenta" no es un diagnóstico; la descomposición te dice qué está lento.

Ejercicio 2 — Cliente vs servidor. En la corrida de esta lección, un cliente solo vio un p50 de 0.229 ms y 60 clientes vieron un p50 de 10.184 ms, contra la misma API sin cambiarla. (a) ¿Se volvió el servidor ~44 veces más lento? (b) ¿A qué se debe la diferencia? (c) ¿Cuál de los dos números le importa al usuario en producción?

Ver solución
  • (a) No. El servidor calcula 2500 * 3 = 7500 igual de rápido en los dos casos; su tiempo de servicio no cambió. Lo confirma el caso de un cliente: 0.229 ms es lo que cuesta el cómputo puro.
  • (b) A la espera en cola. Con 60 clientes compitiendo por atención, cada petición hace fila antes de ser servida. La latencia del cliente es tiempo de servicio + espera en cola, y bajo carga la cola domina.
  • (c) El de 60 clientes (10 ms de p50, 18 ms de p95). En producción hay tráfico concurrente, así que el usuario vive la latencia con cola, no el tiempo de servicio aislado. Medir con un cliente solo subestima lo que la gente realmente experimenta.

Ejercicio 3 — ¿Miente el promedio aquí? Para cada distribución de latencias (en ms), di si el promedio la describe bien o miente, comparándolo con el p50 y el p95. (a) /quote: avg 5.88, p50 5.44, p95 9.43. (b) /quote_slow: avg 28.64, p50 12.18, p95 182.81. (c) Una API hipotética: avg 100, p50 100, p95 105.

Ver solución
  • (a) El promedio describe bien. avg (5.88) ≈ p50 (5.44) y el p95 (9.43) está cerca: la distribución es compacta, sin cola larga. Cuando avg ≈ p50 y el p95 no se dispara, el promedio es un resumen honesto.
  • (b) El promedio miente. avg (28.64) es más del doble del p50 (12.18) y seis veces menor que el p95 (182.81): hay una cola larga que arrastra el promedio a tierra de nadie. Aquí hay que reportar percentiles, no el promedio.
  • (c) El promedio describe bien. avg = p50 = 100 y el p95 (105) está pegadísimo: distribución plana, sin cola. (Es lenta —100 ms— pero consistente; el promedio no engaña sobre la forma.)

La regla mecánica: si avg y p50 casi coinciden y el p95 no se dispara, el promedio es honesto; en cuanto avg se separa del p50, hay cola y el promedio empieza a mentir.

Resumen y siguiente paso

En esta lección abriste el primer instrumento del tablero: la latencia. Aprendiste que en k6 se llama http_req_duration y que se descompone en sending + waiting + receiving, donde waiting es el TTFB —el trabajo real del servidor— y la clave para diagnosticar dónde se va el tiempo. Mediste de verdad la distinción entre la latencia del cliente (lo que ve el usuario: trabajo + espera en cola) y la del servidor (el cómputo puro): la misma Reservo pasó de 0.229 ms con un cliente a 18 ms de p95 con 60, y esa diferencia es cola, no lentitud del servidor.

Y diste el primer golpe a la trampa central del módulo: el promedio miente cuando la latencia tiene cola. Lo viste con números —/quote_slow: promedio 28.64 ms, pero el usuario típico vivió 12 ms y el 5% peor vivió 183 ms—. El promedio cae donde no está casi nadie; la latencia se describe con percentiles. Antes de avanzar deberías poder: nombrar los tres tramos de http_req_duration y qué es el TTFB; explicar por qué la latencia del cliente supera al tiempo de servicio bajo carga; y decir por qué el promedio de una latencia con cola describe a un usuario que no existe.

Lo que sigue es dejar la intuición y pasar a la mecánica. En la lección 3 aprendes a calcular p50, p90, p95 y p99 de verdad con statistics.quantiles: qué devuelve la función, cómo indexar cada percentil, la diferencia entre los métodos inclusive y exclusive, y cómo una sola latencia de la cola dispara el promedio pero apenas mueve la mediana.

Recursos