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

5. La tasa de error y por qué importa

Descripción

El tercer instrumento del tablero es el más fácil de definir y el más fácil de ignorar: la tasa de error, el porcentaje de peticiones que fallaron. En k6 se llama http_req_failed, y es un número entre 0% y 100%. Su definición no tiene misterio; su importancia sí, porque es la métrica que puede convertir en fracaso una prueba que "se ve preciosa" por latencia y throughput. Esta lección instala una regla que parece obvia enunciada y que sin embargo se viola todo el tiempo: una respuesta rápida que devuelve un error no sirvió a nadie. Una latencia bajísima con 10% de errores no es un éxito con un asterisco; es un fracaso, punto.

La razón es que las tres métricas no son independientes: son tres condiciones que se cumplen todas o el sistema falla. La latencia mide cuán rápido respondiste; el throughput, cuánto volumen moviste; la tasa de error, cuántas de esas respuestas fueron de verdad la respuesta correcta y no un error. Si el 10% de tus peticiones devolvió 500, entonces tu p95 de "10 ms" describe la velocidad a la que fallaste una de cada diez veces —velocísimo, sí, pero fallo—. Por eso la tasa de error se lee primero: es el filtro que valida (o invalida) todo lo demás. Lo vas a ver medido con crudeza usando /quote_flaky, el endpoint declarado en la lección 1 que responde rapidísimo pero devuelve 500 en ~10% de las peticiones.

Conexión con el módulo: con esta lección tienes los tres instrumentos completos —latencia (2-3), throughput (4), errores (5)— y ya puedes leer una prueba de carga entera, que es justo lo que hace la lección 6 sobre el resumen de k6. La tasa de error es también la base del segundo tipo de threshold que verás en el módulo 5 (http_req_failed: ['rate<0.01'], "que falle la prueba si más del 1% de las peticiones falla"): aquí aprendes qué es esa tasa; hacer que gatee un deploy es el módulo 5.

El mesero rapidísimo que trae el plato equivocado

Imagina un restaurante que presume de velocidad. El mesero es un rayo: toma tu pedido y en noventa segundos ya tienes un plato en la mesa. La latencia es espectacular. Y el restaurante mueve muchísimas mesas por hora —throughput altísimo—. Pero hay un detalle: una de cada diez veces, el plato que llega no es el que pediste. Pediste risotto y te traen, velozmente, una ensalada que no querías. ¿Dirías que ese mesero es "excelente porque es rápido"? No. Un plato equivocado servido en noventa segundos no es mejor que un plato equivocado servido en diez minutos: en ambos casos no comiste lo que querías. La velocidad de un fallo no lo redime; solo lo entrega más rápido.

Esa es exactamente la tasa de error de una API. Cada petición es un comensal pidiendo un plato (una cotización, una reserva). Una petición exitosa es el plato correcto en la mesa: status 200 con el precio correcto. Una petición fallida es el plato equivocado o la cocina que dice "no puedo": un 500, un timeout, una conexión que se cae. Si tu tasa de error es 10%, uno de cada diez comensales se fue sin su plato —da igual cuán rápido le trajeron el error—. Y aquí está lo cruel de las métricas: la latencia y el throughput solo cuentan los platos que llegaron, correctos o no. Un 500 que sale en 5 ms baja tu latencia promedio (¡es rapidísimo generar un error!) y sube tu throughput (¡cuántas peticiones por segundo!). Las métricas de velocidad y volumen, leídas solas, premian los fallos rápidos. Solo la tasa de error las pone en su sitio.

La tasa de error es el filtro que valida las otras dos métricas. Una petición que falla rápido sigue siendo una petición que falló. Una latencia baja con una tasa de error alta no es un éxito imperfecto: es un fracaso. Lee la tasa de error primero; solo si es aceptable, la latencia y el throughput significan algo.

http_req_failed: qué cuenta como error en k6

En k6, la métrica es http_req_failed, y es una tasa (rate): la proporción de peticiones fallidas, reportada como porcentaje. ¿Y qué cuenta como "fallida"? Por defecto, k6 usa una regla de respuesta (response callback) que considera exitosa toda respuesta con un código de estado HTTP en el rango 2xx-3xx (status < 400), y fallida todo lo demás:

Resultado¿Éxito o fallo por defecto en k6?
200 OK, 201 Created, 301, 304Éxito (status < 400).
400 Bad Request, 401, 403, 404Fallo (error del cliente).
500 Internal Server Error, 502, 503Fallo (error del servidor).
Timeout, conexión rechazada, DNS caídoFallo (la petición ni siquiera obtuvo respuesta).

Dos matices importantes. Primero: por defecto, k6 mira solo el código de estado, no el contenido de la respuesta. Una respuesta 200 con el precio equivocado (price_cents incorrecto) cuenta como éxito para http_req_failed, porque el status fue 200 —verificar que el contenido sea correcto bajo carga es trabajo de un check(), que es el módulo 6—. La tasa de error captura fallos de transporte y disponibilidad (¿respondió el servidor, y con un código sano?), no fallos de lógica (¿respondió lo correcto?). Segundo: en nuestro generador de Python replicamos exactamente esa regla —marcamos ok = (resp.status == 200) y contamos las excepciones (timeouts, conexiones caídas) como fallos—, así que nuestra tasa de error es el equivalente directo de http_req_failed.

Ejemplo trabajado: latencia excelente, y aun así un fracaso

Vamos a medir la lección con dureza. Corremos el generador contra /quote_flaky —el endpoint declarado que responde rápido pero devuelve 500 en ~10% de las peticiones— con 2000 peticiones y 30 clientes, y miramos las tres métricas juntas. El generador cronometra cada latencia, marca si el status fue 200, y al final reporta el RPS, la tasa de error y los percentiles de latencia.

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

def one(url, payload):
    """Devuelve (latencia_ms, ok). ok = True solo si el status fue 200."""
    start = time.perf_counter()
    try:
        req = urllib.request.Request(url, data=payload,
                                     headers={"Content-Type": "application/json"})
        with urllib.request.urlopen(req, timeout=10) as resp:
            resp.read()
            ok = (resp.status == 200)
    except Exception:
        ok = False            # timeout, conexión caída, 500... todo cuenta como fallo
    return (time.perf_counter() - start) * 1000, ok

def load(port, path, total=2000, concurrency=30):
    url = f"http://127.0.0.1:{port}{path}"
    payload = json.dumps({"room": "Focus", "tier": "basic", "hours": 3}).encode()
    latencies, errors = [], 0
    t0 = time.perf_counter()
    with ThreadPoolExecutor(max_workers=concurrency) as pool:
        for latency, ok in pool.map(lambda _: one(url, payload), range(total)):
            latencies.append(latency)
            if not ok:
                errors += 1
    wall = time.perf_counter() - t0
    latencies.sort()
    q = lambda p: statistics.quantiles(latencies, n=100, method="inclusive")[p - 1]
    print(f"endpoint       {path}")
    print(f"RPS            {total / wall:8.1f}")
    print(f"errors         {errors} ({errors / total * 100:.2f}%)")
    print(f"p50            {q(50):8.2f} ms")
    print(f"p95            {q(95):8.2f} ms")

load(PORT, "/quote_flaky")

Qué esperar. La latencia va a salir excelente —el endpoint responde rápido, tanto el 200 como el 500— y sin embargo la tasa de error va a ser ~10%. Esta es la salida real:

endpoint       /quote_flaky
RPS            4761.1
errors         200 (10.00%)
p50               5.45 ms
p95              10.06 ms

Mira lo que tienes delante. Un RPS altísimo (4761), un p50 de 5.45 ms, un p95 de 10.06 ms: por latencia y throughput, esta corrida es indistinguible de una API sanísima. Si solo miraras esas dos métricas, firmarías el deploy. Pero la tasa de error dice 10.00%: 200 de las 2000 peticiones devolvieron 500. Uno de cada diez usuarios que intentó cotizar recibió un error —rapidísimo, eso sí—. ¿Es esta API "rápida"? Es rápida fallando. El p95 de 10 ms describe la velocidad con la que, una de cada diez veces, no entregaste el precio. Este es el escenario de la lección hecho número: latencia preciosa, y un fracaso.

Para que el contraste sea total, compáralo con el /quote normal bajo carga parecida:

endpoint       /quote
RPS            5069.7
errors         0 (0.00%)
p50               5.44 ms
p95               9.43 ms

Las latencias son casi idénticas (p50 5.44 vs 5.45, p95 9.43 vs 10.06) y el throughput también (5069 vs 4761). Por los dos primeros instrumentos, las dos corridas son gemelas. Lo único que las separa es el tercer instrumento: /quote tiene 0% de error y /quote_flaky tiene 10%. Y esa diferencia lo es todo: la primera es una API lista, la segunda es una API rota que además es rápida. Leer la tasa de error es lo que te deja distinguirlas; sin ella, las habrías declarado iguales.

Cómo leer la tasa de error junto a las otras dos

La regla operativa es un orden de lectura, y conviene fijarlo:

  1. Primero, la tasa de error. Si es inaceptable (por encima de tu umbral —típicamente algo como 1%—), la prueba ya falló, y la latencia y el throughput son irrelevantes: no importa cuán rápido y en qué volumen entregaste respuestas si una fracción grande de ellas eran errores. Anota el fracaso y ve a diagnosticar por qué falla.
  2. Solo si la tasa de error es aceptable, lee la latencia (percentiles) y el throughput (RPS). Ahora sí significan algo, porque describen respuestas que de verdad sirvieron al usuario.

Este orden evita la trampa más común de todo el módulo: celebrar una latencia baja que en realidad es la velocidad de un sistema que falla. Y explica por qué en el módulo 5 el threshold de tasa de error (rate<0.01) suele ser el primero que se escribe: es la condición de que la prueba tenga sentido siquiera.

Errores comunes

Mirar la latencia antes que la tasa de error. Qué pasa: el reporte abre con "p95 de 10 ms, excelente" y la tasa de error (10%) aparece tres líneas abajo, ya con el veredicto de "rápida" puesto. Por qué pasa: la latencia es la métrica vistosa; la tasa de error se lee de reojo, si acaso. Cómo detectarlo: si tu conclusión sobre la velocidad se formó antes de mirar http_req_failed, la formaste sobre datos posiblemente contaminados por fallos rápidos. Cómo corregirlo: invierte el orden. Lee la tasa de error primero; solo si pasa, la latencia significa algo.

Creer que un 500 rápido es "medio bueno" porque al menos fue rápido. Qué pasa: "bueno, falla, pero al menos falla rápido, no deja al usuario esperando". Por qué pasa: se aplica la intuición de latencia (rápido = bueno) a un resultado que es un fracaso. Cómo detectarlo: si estás buscando el lado positivo de un error, ya te equivocaste de marco. Cómo corregirlo: para el usuario que quería su cotización, un 500 en 5 ms y un 500 en 5 segundos son el mismo fracaso —no obtuvo el precio—. La velocidad de un error no es un consuelo métrico.

Confundir la tasa de error (http_req_failed) con la corrección del contenido. Qué pasa: la tasa de error es 0% y alguien concluye "todo salió correcto bajo carga", sin notar que la API devolvía 200 con el price_cents equivocado. Por qué pasa: http_req_failed mira el código de estado, no el contenido; un 200 con datos malos cuenta como éxito para esta métrica. Cómo detectarlo: si te importa que el precio sea 7500 y no solo que el status sea 200, http_req_failed no te lo dice. Cómo corregirlo: usa check() (módulo 6) para verificar el contenido bajo carga. La tasa de error valida el transporte; el check valida la lógica.

Ejercicios

Ejercicio 1 — ¿Éxito o fallo por defecto? Para cada respuesta, di si k6 la cuenta como éxito o fallo en http_req_failed con la regla por defecto. (a) 200 OK con {"price_cents": 9999} (el precio correcto era 7500). (b) 503 Service Unavailable. (c) Un timeout: el servidor nunca respondió. (d) 404 Not Found.

Ver solución
  • (a) Éxito. El status es 200 (< 400), así que http_req_failed lo cuenta como exitoso —aunque el precio esté mal—. La tasa de error mira el código, no el contenido; ese precio equivocado lo cazaría un check() (módulo 6), no esta métrica.
  • (b) Fallo. 503 es ≥ 400 (error del servidor): cuenta como fallido.
  • (c) Fallo. Un timeout no obtuvo respuesta; k6 (y nuestro generador) lo cuentan como fallo. En el generador cae en el except y marca ok = False.
  • (d) Fallo. 404 es ≥ 400 (error del cliente): cuenta como fallido por defecto.

Ejercicio 2 — El veredicto correcto. Tienes dos corridas del mismo endpoint. Corrida X: p95 = 10 ms, RPS = 4761, error = 10%. Corrida Y: p95 = 40 ms, RPS = 3000, error = 0%. (a) ¿Cuál está lista para producción, si tu umbral de error es 1%? (b) ¿Por qué la X, siendo más rápida y con más throughput, es la peor? (c) ¿Qué tienen de engañoso el p95 y el RPS de la X?

Ver solución
  • (a) La Y. Tiene 0% de error (bajo el umbral de 1%) y una latencia perfectamente aceptable (p95 40 ms). La X tiene 10% de error, muy por encima del 1%: ya falló, aunque sea más rápida.
  • (b) Porque la velocidad y el volumen de la X describen, en buena parte, fallos rápidos. Un p95 de 10 ms con 10% de error significa que 1 de cada 10 usuarios recibió un 500 velocísimo. La Y es un poco más lenta pero funciona; la X es rápida pero rota.
  • (c) Que ambos están inflados por los errores: generar un 500 es más rápido que calcular y devolver el precio real, así que los fallos bajan el p95 y suben el RPS de la X. Sus métricas de velocidad premian, perversamente, sus fallos.

Ejercicio 3 — El orden de lectura. Recibes este resumen de una prueba: p50 = 6 ms, p95 = 12 ms, RPS = 5000, http_req_failed = 8%. (a) ¿Cuál es la primera métrica que debes leer y qué te dice? (b) ¿Deberías siquiera fijarte en el p95 para decidir? (c) Escribe el veredicto en una frase.

Ver solución
  • (a) La tasa de error (http_req_failed = 8%). Te dice que 8 de cada 100 peticiones fallaron: muy por encima de un umbral típico del 1%. La prueba ya falló aquí.
  • (b) Para decidir si está lista, no: con 8% de error el veredicto ya es "no lista", y el p95 no lo cambia. El p95 y el RPS son útiles luego, para diagnosticar (¿los errores vienen de saturación? ¿de un servicio caído?), pero no rescatan la decisión.
  • (c) "No lista: 8% de tasa de error (umbral 1%); la latencia baja (p95 12 ms) solo describe la velocidad a la que falla 1 de cada 12 peticiones."

Resumen y siguiente paso

En esta lección cerraste el tablero con el tercer instrumento: la tasa de error (http_req_failed en k6), el porcentaje de peticiones fallidas. Aprendiste qué cuenta como fallo por defecto (status ≥ 400, timeouts, conexiones caídas) y qué no (un 200 con contenido equivocado, que es trabajo de un check). Y clavaste la regla central con números: /quote_flaky dio un p50 de 5.45 ms y un p95 de 10 ms —latencia indistinguible de una API sana— pero 10% de error, mientras /quote daba la misma latencia con 0%. Por los dos primeros instrumentos eran gemelas; solo la tasa de error reveló que una estaba lista y la otra rota.

La lección de fondo es el orden de lectura: la tasa de error primero, porque valida (o invalida) todo lo demás; una respuesta rápida que falla no sirvió a nadie, y su velocidad hasta infla el p95 y el RPS a la baja. Antes de avanzar deberías poder: definir http_req_failed y decir qué cuenta como error; explicar por qué una latencia baja con 10% de error es un fracaso; y enunciar el orden en que se leen las tres métricas.

Ya tienes los tres instrumentos completos: latencia, throughput y errores. Lo que sigue es leerlos todos juntos en su formato nativo. En la lección 6 abrimos el resumen de k6 run línea por línea —como contenido rotulado, porque k6 no está instalado— y mapeamos cada línea (http_req_duration, http_reqs, http_req_failed, iterations, vus) a las métricas que ya calculaste tú mismo en Python.

Recursos