Módulo 5: Thresholds — pasa/falla y SLOs
2. Qué es un threshold y el pasa/falla
Descripción
Un threshold —umbral— es la pieza más pequeña y más poderosa de este módulo: una regla sobre una métrica que convierte una medición en un veredicto binario. "El p95 debe estar por debajo de 200 ms" es un threshold. No es un objetivo aspiracional ni una nota al pie de un informe: es una condición que la prueba verifica, y de la que sale un solo bit de información —pasa o falla—. En esta lección definimos con precisión qué es un threshold, cómo se lee, y por qué el pasa/falla es tan valioso; y construimos la primera versión ejecutable del espejo en Python: una función que toma las latencias que realmente mediste contra Reservo y devuelve True (pasa) o False (falla). La ves pasar con carga liviana y fallar con carga pesada, sobre números de verdad. La consecuencia mecánica de ese veredicto —convertirlo en un código de salida que hace fallar el CI— es la lección 4; aquí nos concentramos en el juicio en sí: la regla y el bit que produce.
Conexión con el módulo: la lección 1 explicó por qué queremos juzgar (pasar de medir a gatear); esta explica qué es el juicio (el threshold) y produce su primera forma ejecutable. La métrica que ponemos bajo umbral —el p95— viene del módulo 3; el threshold no la reinventa, le pone una regla. En la lección 3 verás cómo k6 declara estos mismos thresholds (como contenido) para las tres métricas clave; en la 4, cómo el veredicto se vuelve un exit code. Aquí construimos el corazón: la función que mira un número y dice pasa o falla.
El inspector con la plantilla
Piensa en un inspector de calidad al final de una línea de producción de tornillos. No es un artista que "siente" si un tornillo está bien: tiene una plantilla —una placa metálica con un agujero de un diámetro exacto—. El tornillo pasa por el agujero: bien. No pasa: mal. La plantilla convierte una propiedad continua (el diámetro, que podría ser 4.98 mm, 5.01 mm, 5.13 mm...) en una decisión de un solo bit (pasa / no pasa). El inspector no reporta "este tornillo mide 5.13 mm y me parece un poco grande, ¿qué opinas?"; reporta rechazado, y el tornillo va al contenedor de descarte. La plantilla es objetiva (cualquier inspector con la misma placa da el mismo veredicto), rápida (un gesto por tornillo) y accionable (el veredicto dispara una acción: aceptar o descartar).
Un threshold es esa plantilla, aplicada a una métrica de rendimiento. La métrica continua es el p95 (podría ser 9 ms, 199 ms, 247 ms...); la plantilla es la regla p(95) < 200; el veredicto es pasa/falla. Igual que la placa del inspector, un threshold es objetivo (el mismo p95 y la misma regla dan siempre el mismo veredicto, sin opiniones), rápido (una comparación) y, sobre todo, accionable —el pasa/falla va a disparar algo, que en la lección 4 será detener o autorizar el deploy—. Toda la potencia de este módulo nace de haber reducido "¿el rendimiento está bien?" a un bit que una máquina puede leer y sobre el que puede actuar.
La anatomía de un threshold
Un threshold tiene tres partes, y conviene nombrarlas porque las vas a ver una y otra vez:
- La métrica. Qué se mide. En este módulo, tres: la latencia (p95 de
http_req_duration), la tasa de error (http_req_failed) y la tasa de checks correctos (checks). El threshold no crea la métrica —eso lo hiciste en el módulo 3—; la usa. - El operador y el valor —el umbral propiamente dicho. La comparación que define "bien".
< 200(menor que 200 ms),< 0.01(menor que 1%),> 0.99(mayor que 99%). El valor es una decisión (de dónde sale ese 200 es la lección 5); el operador dice de qué lado está lo aceptable. - El veredicto. El bit que produce:
True/False, pasa/falla, verde/rojo. Es lo único que sale del threshold, y es lo único que el pipeline necesita.
Escrito como una frase en español, un threshold es siempre de la forma: "la métrica debe estar operador valor". "El p95 debe estar por debajo de 200 ms." "La tasa de error debe estar por debajo del 1%." "La tasa de checks debe estar por encima del 99%." Si puedes decir esa frase, puedes escribir el threshold. Y si la métrica medida cumple la frase, pasa; si no, falla. No hay una tercera opción, y esa ausencia de tercera opción es exactamente el punto: elimina el "más o menos", el "depende", el "yo lo veo bien". Un threshold no negocia.
El espejo ejecutable: evaluate_thresholds en Python
Vamos a construir el threshold con las manos, en Python, para que no sea magia. La función evaluate_thresholds recibe las métricas ya medidas (la lista de latencias, la tasa de error, la tasa de checks —todo real, medido contra Reservo con las técnicas de los módulos 3 y 4—) y aplica las tres reglas. Por ahora se concentra en producir el veredicto (devuelve True si todos pasan, False si alguno falla) e imprimir PASS/FAIL por cada regla. En la lección 4 le añadiremos el sys.exit que convierte ese True/False en un código de salida.
import statistics
def q(data, p):
"""Percentil p (1-99) con el metodo inclusivo (modulo 3)."""
return statistics.quantiles(data, n=100, method="inclusive")[p - 1]
def evaluate_thresholds(latencies, error_rate, checks_rate, p95_limit_ms):
"""Evalua las metricas medidas contra los umbrales. Imprime PASS/FAIL por
umbral y devuelve True solo si TODOS pasan (el veredicto del gate)."""
p95 = q(latencies, 95)
# Cada threshold: (etiqueta, paso el umbral?, valor medido para mostrar).
checks = [
(f"http_req_duration: p(95) < {p95_limit_ms:.0f}ms",
p95 < p95_limit_ms,
f"p(95) = {p95:.2f}ms"),
("http_req_failed: rate < 1.00%",
error_rate < 0.01,
f"rate = {error_rate:.2%}"),
("checks: rate > 99.00%",
checks_rate > 0.99,
f"rate = {checks_rate:.2%}"),
]
all_pass = True
for label, passed, measured in checks:
status = "PASS" if passed else "FAIL"
if not passed:
all_pass = False # UN threshold roto basta para reprobar todo
print(f"{label:<38} {measured:<18} {status}")
return all_pass
Léela como la frase de antes. La primera regla dice "el p95 debe estar por debajo de p95_limit_ms": calcula p95 = q(latencies, 95) (el percentil real de tus latencias) y compara p95 < p95_limit_ms. La segunda, "la tasa de error debe estar por debajo del 1%": error_rate < 0.01. La tercera, "la tasa de checks debe estar por encima del 99%": checks_rate > 0.99. Cada una produce un booleano —su veredicto—. Y hay una decisión de diseño crucial en la línea if not passed: all_pass = False: basta con que UN threshold falle para que el veredicto global sea "falla". Un gate es una conjunción, un Y lógico: pasa solo si todas las reglas pasan. Esto es igual que en la fábrica —una botella con el peso perfecto pero mal tapada se descarta igual—; y es igual en k6, donde si cualquier threshold falla, la prueba entera falla.
Esa función es el threshold hecho código, y lo más importante es que no tiene nada de mágico: es tres comparaciones y un and implícito. Cuando en la lección 3 veas el bloque options.thresholds de k6, reconocerás exactamente estas tres reglas —k6 las escribe más compacto, pero hacen esto—.
Verlo pasar y fallar (ejecutado de verdad)
Envolvemos evaluate_thresholds en un pequeño runner que primero mide (lanza carga contra Reservo, junta las latencias, cuenta errores y checks correctos) y luego evalúa. Ese runner es threshold_gate.py; lo usamos entero en la lección 4, aquí nos basta ver su veredicto. Todo lo que sigue es salida real, ejecutada contra la API de Reservo en localhost, con /quote_cpu (el endpoint que el módulo declaró en la lección 1).
Primero, carga liviana: 400 peticiones con 4 clientes concurrentes. Con tan poca concurrencia, /quote_cpu casi no sufre contención por el GIL, así que su p95 se queda en pocos milisegundos.
Qué esperar — las tres reglas pasan (p95 muy por debajo de 200 ms, cero errores, checks al 100%); el veredicto es PASS:
$ python3.14 threshold_gate.py http://127.0.0.1:PORT /quote_cpu 400 4 200
# /quote_cpu | 400 peticiones, concurrencia 4
THRESHOLD MEDIDO RESULTADO
----------------------------------------------------------------------
http_req_duration: p(95) < 200ms p(95) = 9.74ms PASS
http_req_failed: rate < 1.00% rate = 0.00% PASS
checks: rate > 99.00% rate = 100.00% PASS
----------------------------------------------------------------------
GATE: PASS (exit code 0)
Ahora, la misma regla, el mismo endpoint, pero con carga pesada: 2000 peticiones con 120 clientes concurrentes. El GIL serializa el trabajo de CPU, la fila crece, y el p95 se dispara.
Qué esperar — el p95 cruza los 200 ms; esa sola regla falla, y con eso el veredicto global es FAIL (las otras dos siguen pasando):
$ python3.14 threshold_gate.py http://127.0.0.1:PORT /quote_cpu 2000 120 200
# /quote_cpu | 2000 peticiones, concurrencia 120
THRESHOLD MEDIDO RESULTADO
----------------------------------------------------------------------
http_req_duration: p(95) < 200ms p(95) = 246.96ms FAIL
http_req_failed: rate < 1.00% rate = 0.00% PASS
checks: rate > 99.00% rate = 100.00% PASS
----------------------------------------------------------------------
GATE: FAIL (exit code 1)
Detente en lo que acaba de pasar, porque es la lección entera. La app no cambió. La regla no cambió (p(95) < 200 en las dos corridas). Lo único que cambió fue la carga: de 4 a 120 clientes concurrentes. Y el veredicto pasó de verde a rojo. Eso es un threshold haciendo su trabajo: traducir una diferencia de carga en una diferencia de veredicto, sin que nadie interprete nada. Con carga liviana la app cumple el SLO; con carga pesada, no; y el gate lo dice en una palabra.
Fíjate también en la segunda corrida: solo el threshold de latencia falló —el de error y el de checks siguieron en verde—, y aun así el gate entero reprobó. Ese es el Y lógico en acción: un gate no promedia sus reglas ni "redondea a favor". Una sola regla rota tiñe de rojo todo el veredicto. Es severo a propósito: la calidad es una conjunción de condiciones, no un puntaje que se compensa.
Por qué el binario vale tanto
Podrías preguntarte: ¿no perdemos información al reducir un p95 rico y matizado a un solo bit? Sí, y ese es exactamente el punto. La riqueza del p95 (que fue 246.96 ms, con tal distribución, tal cola...) es valiosísima cuando un humano investiga —la usarás en el módulo 7 para encontrar el cuello de botella—. Pero para decidir automáticamente si el deploy avanza, un humano no está en el bucle, y una máquina no puede actuar sobre "246.96 ms con una cola preocupante": necesita un bit. El threshold es el traductor entre esos dos mundos. Guarda el número completo para el análisis, y produce el bit para la decisión.
Ese bit es lo que hace el rendimiento automatizable. Un número requiere un ojo humano que lo interprete; un bit requiere solo un if. Al convertir "¿el rendimiento está bien?" en un booleano, el threshold permite que la respuesta viaje por un pipeline, dispare una acción y proteja producción sin intervención —a la velocidad de la máquina, en cada cambio, sin cansarse—. Perder los matices en el punto de decisión no es un defecto: es la condición para poder decidir a máquina.
Errores comunes
Confundir un objetivo con un threshold. Qué pasa: un equipo dice "nuestro objetivo es p95 bajo 200 ms" y lo escribe en una wiki, pero la prueba de carga no lo verifica —solo reporta el número—. Por qué pasa: se confunde aspirar a algo con hacer cumplir algo. Cómo detectarlo: si tu "objetivo" no puede reprobar una corrida (devolver False, poner el build rojo), es un deseo, no un threshold. Cómo corregirlo: escríbelo como una regla que la prueba evalúa y de la que sale un pasa/falla —una comparación en evaluate_thresholds, un renglón en options.thresholds—.
Creer que un gate promedia sus reglas. Qué pasa: alguien ve dos thresholds en verde y uno en rojo y concluye "dos de tres, va bien, pasa". Por qué pasa: se piensa el gate como un puntaje. Cómo detectarlo: si tu lógica de veredicto no reprueba con una sola regla rota, está mal. Cómo corregirlo: un gate es un Y lógico —all_pass empieza en True y cualquier not passed lo pone en False—. Una regla rota reprueba todo; no hay compensación entre thresholds.
Poner el umbral sobre el promedio en vez del percentil. Qué pasa: alguien escribe la regla sobre la latencia promedio ("avg < 200") y la corrida pasa aunque el 5% de los usuarios sufra segundos de espera. Por qué pasa: el promedio esconde la cola (módulo 3). Cómo detectarlo: si tu threshold de latencia usa el promedio, estás gateando la métrica equivocada. Cómo corregirlo: pon el umbral sobre un percentil (p95, p99) —es lo que describe la experiencia del usuario que sufre, y es lo que k6 y las SLO de latencia usan—.
Ejercicios
Ejercicio 1 — Traduce a la frase. Escribe cada uno de estos thresholds como la frase "la métrica debe estar operador valor", y di qué métrica, operador y valor tiene. (a) p(95) < 200. (b) rate < 0.01 sobre http_req_failed. (c) rate > 0.99 sobre checks.
Ver solución
- (a) "El p95 de la latencia debe estar por debajo de 200 ms." Métrica: p95 de
http_req_duration. Operador: menor que. Valor: 200 ms. - (b) "La tasa de error debe estar por debajo del 1%." Métrica:
http_req_failed. Operador: menor que. Valor: 0.01 (1%). - (c) "La tasa de checks correctos debe estar por encima del 99%." Métrica:
checks. Operador: mayor que. Valor: 0.99 (99%).
Nota que dos usan "menor que" (queremos poca latencia y pocos errores) y uno "mayor que" (queremos muchos checks correctos). El operador va según qué dirección de la métrica es "buena".
Ejercicio 2 — Emite el veredicto. Con los tres thresholds p(95)<200, error<0.01, checks>0.99, di el veredicto global (PASS/FAIL) de cada corrida medida y cuál(es) regla(s) falló(aron). (a) p95=9.74 ms, error=0.00%, checks=100%. (b) p95=246.96 ms, error=0.00%, checks=100%. (c) p95=150 ms, error=0.00%, checks=98.5%.
Ver solución
- (a) PASS. Las tres pasan (9.74<200, 0<0.01, 1.00>0.99). Verde.
- (b) FAIL. Falla solo la latencia (246.96 ≥ 200); error y checks pasan. Pero una regla rota reprueba todo → FAIL.
- (c) FAIL. Falla solo checks (0.985 no es > 0.99); latencia y error pasan. Otra vez, una regla rota → FAIL. (Un p95 excelente no salva a un gate cuyo 1.5% de respuestas fue incorrecto.)
Ejercicio 3 — Añade un cuarto threshold. Quieres añadir a evaluate_thresholds una cuarta regla: "el p99 debe estar por debajo de 500 ms". Escribe la tupla (etiqueta, paso?, medido) que añadirías a la lista checks, usando el helper q. ¿Por qué querrías vigilar el p99 además del p95?
Ver solución
(f"http_req_duration: p(99) < 500ms",
q(latencies, 99) < 500,
f"p(99) = {q(latencies, 99):.2f}ms"),
Se añade a la lista checks y el resto de la función no cambia: el bucle la evalúa y el all_pass la incluye en el Y lógico. Querrías vigilar el p99 además del p95 porque el p95 protege a "casi todos" (19 de cada 20) pero deja libre al peor 5%; el p99 pone un techo también a la cola extrema (1 de cada 100). En sistemas donde el usuario de la cola importa mucho (un pago, un login), poner umbral al p99 evita que la app cumpla el p95 mientras castiga con esperas larguísimas a una minoría.
Resumen y siguiente paso
Un threshold es una regla sobre una métrica que produce un veredicto binario: pasa o falla. Tiene tres partes —la métrica (qué se mide), el operador y el valor (el umbral, "la métrica debe estar operador valor") y el veredicto (el bit que sale)— y su valor está en ser objetivo, rápido y accionable. Lo construiste con las manos en Python: evaluate_thresholds toma las latencias, la tasa de error y la tasa de checks realmente medidas, aplica las tres reglas, imprime PASS/FAIL por cada una y devuelve True solo si todas pasan —un Y lógico, donde una sola regla rota reprueba todo el gate—.
Y lo viste funcionar: la misma app y la misma regla (p(95) < 200), verde con carga liviana (p95 = 9.74 ms) y roja con carga pesada (p95 = 246.96 ms), donde lo único que cambió fue la concurrencia. El binario que produce el threshold pierde los matices del número a propósito: esa pérdida es lo que hace el rendimiento automatizable, porque una máquina puede actuar sobre un bit, no sobre "246.96 ms con una cola preocupante".
Antes de avanzar deberías poder: escribir cualquier threshold como la frase "la métrica debe estar operador valor"; explicar por qué un gate es un Y lógico y no un promedio; y leer el veredicto de una corrida diciendo qué regla(s) falló(aron). Lo que sigue, en la lección 3, es ver cómo k6 declara estos mismos tres thresholds —http_req_duration, http_req_failed, checks— en su bloque options.thresholds (como contenido rotulado), y mapear cada uno a la regla de Python que acabas de escribir. La misma plantilla del inspector, en el idioma industrial.
Recursos
- k6 — Thresholds — la referencia oficial: cómo k6 define un threshold como una regla que la prueba debe cumplir. La forma industrial de la
evaluate_thresholdsque construiste aquí. statistics.quantiles— documentación de Python — cómo el helperqcalcula el p95 real sobre el que se aplica el threshold. La métrica que la regla juzga.- Google SRE Book — Service Level Objectives — por qué una condición de rendimiento se expresa como una regla verificable (un SLO) y no como un deseo. El fundamento de "un threshold no negocia".
- k6 —
check()— la métricachecks(tasa de verificaciones correctas) que uno de los thresholds vigila; su uso a fondo para verificar corrección bajo carga es el módulo 6.