SenseiTeoría desde cero0/27

4 · La red y la webLección 5 de 6

Timeouts, reintentos e idempotencia

Timeouts, retries and idempotency

Cuánto esperar a otro servicio, cuándo volver a intentar y cómo hacerlo sin publicar dos veces ni tumbar todo con reintentos: timeouts, deadlines, backoff con jitter, idempotencia, circuit breakers y degradación, con los números de C01.

⏱ 59 min lo esencial · 72 min todo15 conceptos nuevos2 calculadoras9 ejerciciosAPI
Secciones14
  1. 01Un servicio lento es peor que uno caído: el timeout
  2. 02El deadline: el tiempo que queda viaja con la request
  3. 03Reintentar: cuándo sí y cuándo no
  4. 04Cuánto esperar: backoff exponencial, jitter y Retry-After
  5. 05La tormenta de reintentos y su presupuesto
  6. 06Idempotencia: repetir sin daño
  7. 07Cuando la dependencia falla: circuit breaker, load shedding, degradación y fail-open
  8. 08Hedging: pedir dos veces para recortar la cola extra
  9. 09Errores comunes
  10. 10En la vida real extra
  11. 11Ejercicios
  12. 12Lo que sigue
  13. 13Palabras que vas a oír
  14. 14Chequeo
La pregunta

Cuando Triton no contesta, ¿cuánto debe esperar el gateway, cuándo vuelve a intentar y cómo evita publicar dos veces o tumbar todo con reintentos?

Ana toca «Publicar» con la foto de sus tenis. El backend del marketplace (el programa que recibe lo que ella publica; aquí le diremos el caller, «el que llama») le pide la categoría al gateway de C01. El gateway prepara la foto y se la manda a Triton, el servidor que corre la ResNet-50 en la GPU. Casi siempre, todo eso toma unos 50 ms.

Pero un día Triton se traba: la GPU se queda ocupada, o su máquina se reinicia. En ese momento el gateway tiene que decidir tres cosas, y de ellas depende que el problema se quede en un mal rato o se vuelva una caída de todo el servicio:

  • Cuánto esperar. ¿Espera a Triton 150 ms, 5 segundos o para siempre?
  • Cuándo volver a intentar. ¿Reintenta de inmediato, en un rato o nunca?
  • Cómo no empeorar las cosas. Si reintenta, ¿cómo evita que la publicación de Ana salga dos veces, o que miles de reintentos juntos terminen de tumbar a Triton?

Las piezas ya las tienes: los códigos de HTTP, el await con límite de la lección de async y el reintento como réplica en el tiempo de la de probabilidad. Aquí se juntan.

01

Un servicio lento es peor que uno caído: el timeout

Esencial

A slow service is worse than a dead one: the timeout

Un servicio caído falla rápido. Si no hay ningún programa escuchando en su puerto, la conexión se rechaza en un milisegundo y el gateway pasa a su plan B. Un servicio lento no avisa nada: la request se queda esperando, y mientras espera ocupa cosas. Memoria para la foto, una conexión abierta, uno de los 16 lugares que el gateway de C01 reserva para requests en vuelo, las que ya entraron y todavía no terminan (su MAX_INFLIGHT). La Ronda (M03) lo resume en una frase: un servicio lento es peor que uno caído.

Un timeout es el tiempo máximo que un programa espera algo antes de rendirse. Cuando vence, el programa deja de esperar, cancela la operación y la trata como una falla. Sin timeout, la espera no tiene fin. Y no es un caso raro: en Python, la biblioteca requests no trae ninguno si no se lo pones, así que una llamada puede quedarse colgada para siempre.

Tres relojes en una sola llamada

Una llamada por la red pasa por etapas, y cada una se puede trabar de forma distinta. Por eso las bibliotecas suelen tener más de un timeout:

  • Timeout de conexión (connect timeout): cuánto esperar a que la otra máquina acepte la conexión, el saludo inicial de TCP que viste como handshake. Dentro de un mismo centro de datos ese saludo toma menos de un milisegundo, así que este reloj puede ser corto. Si vence, la request nunca llegó.
  • Timeout de lectura (read timeout): ya conectado, cuánto esperar a que llegue el siguiente pedazo de la respuesta. Ojo: en muchas bibliotecas cuenta el silencio entre pedazos, no el total. Un servidor que manda un byte cada 9 segundos nunca dispara un timeout de lectura de 10 s.
  • Timeout total: el tope de la llamada completa, de principio a fin. Es el que le importa a quien espera, y el único que acota de verdad cuánto dura todo.
Una llamada del gateway a Triton, de izquierda a derecha en el tiempo, con cuatro etapas: conectar (el saludo), mandar la foto, esperar a que Triton calcule y leer la respuesta, que llega en cinco pedazos. Abajo, los tres timeouts. El de conexión cubre solo el saludo del principio. El de lectura cubre el silencio antes del primer pedazo y se reinicia con cada pedazo que llega. El total cubre la llamada completa, de punta a punta. Una respuesta que llega a goteo, un byte cada 9 segundos, nunca dispara un timeout de lectura de 10 segundos; solo el total la corta. UNA LLAMADA, TRES RELOJES saludo mandar esperar a Triton leer a pedazos TIEMPO Conexión solo el saludo del principio Lectura el silencio más largo entre pedazos; vuelve a cero con cada pedazo que llega Total la llamada completa, de punta a punta A goteo, un byte cada 9 s nunca dispara una lectura de 10 s. Solo el total acota cuánto dura todo.
Una llamada del gateway a Triton, de izquierda a derecha en el tiempo. El reloj de conexión solo cuida el saludo; el de lectura se reinicia con cada pedazo que llega; el total cubre todo. Una respuesta que llega a goteo puede burlar al de lectura, pero no al total.

Lo que cuesta esperar de más

La Ley de Little dice cuántas requests hay adentro en promedio: las que llegan por segundo por lo que tarda cada una. Un timeout acota lo que tarda cada una cuando la pieza de abajo se traba, y con eso acota cuántas se juntan. En la lección de async lo contaste: a 500 por segundo, con Triton trabado, un timeout de 150 ms deja 75 llamadas colgadas, y uno de 5 s, 2,500. Ahora mira qué hace eso con los 16 lugares de cada pod (cada una de las 5 copias del gateway en Kubernetes). Cuando están llenos, la siguiente request recibe un 503, el código de «estoy saturado»:

Triton trabado, en uno de los 5 pods del gatewayLa cuenta
Requests que le llegan a cada pod500 por segundo entre 5 pods
500 ÷ 5
100/s
Con timeout de 150 ms, lo que dura cada requestlos 50 ms de siempre, menos los ≈ 10 de Triton, más el timeout
50 − 10 + 150
190 ms
En vuelo por podLittle; el pod tiene 16 lugares
100 × 0.19 s
19
Lo más que despacha el podcada lugar se libera a los 190 ms
16 ÷ 0.19 s
≈ 84/s
Rechazadas al instante con 503
1 − 84 ÷ 100
≈ 16 %
Con timeout de 5 s, en vuelo por podpero solo hay 16 lugares
100 × 5.04 s
504
Lo más que despacha el pod
16 ÷ 5.04 s
≈ 3.2/s
Rechazadas al instante con 503
1 − 3.2 ÷ 100
≈ 97 %
Resultado
Con 150 ms, todas reciben su respuesta antes de los 300 ms del caller: 84 de cada 100, un 504 a los ≈ 190 ms, y las otras 16, un 503 al instante. El caller muestra el selector de categorías. Con 5 s, 97 de cada 100 se rechazan y las 3 que entran esperan 5 s una respuesta que nadie va a leer, porque el caller se fue a los 300 ms.

Con 150 ms no caben todas: 19 en vuelo contra 16 lugares, como contaste en la lección de async, y ≈ 16 % recibe un 503. No es una falla del diseño: es el tope haciendo su trabajo, porque cada request recibe su respuesta a tiempo y el caller pasa a su plan B. Sin el tope de 16, con el timeout de 5 s, sería mucho peor: 504 fotos de hasta 10 MB son hasta 5 GB, en un pod de 1 GiB.

Para ir más hondoLos timeouts que traen las bibliotecas

Cada biblioteca trae su propio valor por omisión, y casi nunca es el que quieres. Estos son los que documenta cada una en 2026. requests no trae ninguno: sin timeout, espera para siempre. httpx, otra biblioteca de Python, trae 5 segundos para cada etapa (conectar, mandar, leer cada pedazo). aiohttp, otra pensada para async, trae un total de 5 minutos. Un servidor intermedio, como un balanceador de carga, suele cortar las conexiones que pasan cierto tiempo sin actividad: el balanceador de aplicaciones de AWS (ALB, en la nube de Amazon), a los 60 s por omisión. Ninguno de esos números sabe que tu caller se rinde a los 300 ms. Por eso la Ronda pide que cada llamada que sale de tu programa lleve un timeout explícito, escrito en el código o en la configuración (en C01, INFER_TIMEOUT_S=0.15).

02

El deadline: el tiempo que queda viaja con la request

Esencial

The deadline: the time left travels with the request

Ponerle un timeout a cada llamada no basta: hay que elegirlos bien. Si el caller espera 300 ms y el gateway le da 5 s a Triton, cuando Triton se traba el gateway sigue esperando mucho después de que el caller se fue.

La solución es pensar en un deadline (fecha límite): el momento en que la request entera deja de importar, no una duración por salto. Cada salto calcula cuánto le queda y espera como máximo eso:

timeout de una llamada = min(su propio tope, lo que queda del deadline)

Así, ningún salto espera más de lo que le queda a quien lo llamó. La Ronda lo dice como regla: cada capa se rinde antes que la de arriba.

La cadena de C01

En la lección de async viste la cadena de C01: «300 → 250 → 150 → 100 ms» (M15). Ahora puedes leer por qué tiene cada número:

  • 300 ms, el caller. El backend del marketplace se rinde a los 300 ms y muestra el selector de categorías. Es el techo de todo lo demás.
  • 250 ms, el deadline del gateway. El gateway se rinde 50 ms antes que el caller. Esos 50 ms le alcanzan para contestar un 504 (Gateway Timeout, «se me acabó el tiempo esperando al de atrás») antes de que el caller abandone.
  • 150 ms, la llamada a Triton. Es el tope de esa llamada. Lo que sobra de los 250 se usa para recibir y preparar la foto antes, y para contestar después.
  • 100 ms, la fila de Triton. Triton rechaza lo que lleva más de 100 ms esperando su turno, con su política de fila REJECT, porque ya no llegaría a tiempo.
La cadena de relojes de C01: quién se rinde primero · Toca cada caja para ver su reloj. «Un día normal» recorre la request de Ana en ≈ 50 ms; «Triton se traba» muestra quién se rinde primero y qué ve Ana.
  • App de Ana · toca «Publicar»
    Ana sube la foto de sus tenis desde la app. Ella no ve ningún reloj: ve una sugerencia de categoría o el selector para elegirla a mano.
  • Backend · se rinde a los 300 ms
    El programa del marketplace que recibe la publicación. Le pide la categoría al gateway y espera como máximo 300 ms. Es el techo de todos los demás relojes: si vence, o si llega un 503 o un 504, muestra el selector.
  • Selector manual · el plan B
    El fallback de C01: Ana elige la categoría a mano. No depende del modelo, así que publicar sigue funcionando aunque Triton esté caído. Cada vez que se usa se cuenta, por motivo, para que una caída no pase en silencio.
  • Gateway · deadline de 250 ms
    Al llegar la request anota su deadline: 250 ms, 50 menos que el backend, para alcanzar a contestar un 504 antes de que el backend se vaya. Prepara la foto y llama a Triton con min(150, lo que queda). Tiene 16 lugares por pod (MAX_INFLIGHT): la 17 recibe un 503 al instante.
  • Fila de Triton · rechaza a los 100 ms
    Las fotos esperan su turno para la GPU. Triton rechaza la que lleva más de 100 ms en la fila (su política REJECT), porque ya no llegaría dentro de los 150 ms del gateway: calcularla sería trabajo zombi.
  • ResNet-50 · en la T4
    Triton corre el modelo en la GPU. En un día normal, ir y volver a Triton toma ≈ 10 ms. Cuando se traba, es la pieza que todos los relojes de arriba protegen.
Cuánto le queda a la llamada a TritonLa cuenta
Deadline del gatewayempieza cuando llega la request
—
250 ms
Un día normal: recibir y preparar la fotoun supuesto para el ejemplo
250 − 20
quedan 230 ms
Timeout de la llamada
min(150, 230)
150 ms
Una foto pesada que tardó en subir y decodificar
250 − 140
quedan 110 ms
Timeout de la llamada
min(150, 110)
110 ms
Si Triton agotó sus 150 ms y se quisiera reintentar
250 − 20 − 150
quedan 80 ms
Resultado
El tope de 150 ms se recorta cuando la request ya gastó tiempo. Y después de un timeout, a un segundo intento le quedan ≈ 80 ms, menos de lo que no le alcanzó al primero.

El trabajo zombi

¿Qué pasa si un salto espera más que el de arriba? Que trabaja para nadie. Si el caller ya se rindió y Triton sigue calculando, la GPU gasta tiempo en una respuesta que nadie va a leer. La Ronda le llama trabajo zombi, y en una sobrecarga es justo lo que no sobra: cada lugar de la fila ocupado por un zombi es una request viva que no entra.

Por eso, cuando un salto se rinde, conviene que cancele hacia abajo: cerrar la conexión o mandar un aviso para que el de atrás deje de trabajar. Es la misma cancelación de asyncio.wait_for de la lección de async. En los LLMs importa todavía más: si la cancelación no llega, la GPU sigue generando tokens para nadie.

Para ir más hondoCómo viaja el deadline de una máquina a otra

Con gRPC, el deadline viaja solo: la biblioteca lo manda en cada llamada, en un header llamado grpc-timeout (por ejemplo, grpc-timeout: 190m, 190 milisegundos), y el servidor sabe cuánto le queda. Con HTTP lo haces tú. Al llegar la request, el gateway anota «termino a más tardar en 250 ms» y, antes de cada llamada, calcula lo que queda.

Un detalle: entre máquinas se manda lo que queda (una duración, como «190 ms»), no la hora límite. Los relojes de dos máquinas nunca marcan exactamente lo mismo, y un desfase de unos milisegundos ya rompería un deadline de 250. Dentro de la misma máquina se mide con un reloj monótono, uno que solo avanza (en Python, time.monotonic()), no con la hora del día, que puede brincar si el sistema la corrige.

03

Reintentar: cuándo sí y cuándo no

Esencial

Retrying: when yes and when no

Un reintento (retry) es volver a mandar una request que falló, con la esperanza de que la segunda vez salga bien. Funciona cuando la falla es pasajera (transient): un pod que se estaba reiniciando, una conexión que se cayó, un servidor saturado unos segundos. No sirve cuando la falla es permanente: si la request está mal hecha, repetirla da el mismo error.

En la lección de probabilidad lo contaste como una réplica en el tiempo: si cada intento falla el 1 % de las veces, por su lado, dos fallan juntos el 0.01 × 0.01 = 0.01 %. La letra chica es «por su lado». Si falló porque el servidor está caído o saturado, el segundo intento lo encuentra igual.

Qué dice cada falla

Cada respuesta HTTP trae un código de estado de tres cifras. Los que empiezan con 4 dicen «el problema es tu request»; los que empiezan con 5, «el problema es mío». Eso ya decide mucho:

Lo que pasóQué quiere decir¿Reintentar?
Conexión rechazadaNo había nadie escuchando: el pod se reiniciaSí, mejor contra otra réplica. La request nunca llegó, así que repetirla es seguro
503 Service Unavailable«Estoy saturado o todavía no estoy listo»Sí, después de esperar, y respetando Retry-After si viene
429 Too Many Requests«Pasaste tu límite de requests»Sí, más despacio, respetando Retry-After
500 Internal Server ErrorAlgo se rompió adentro del servidorComo mucho una vez: a veces es pasajero, a veces un error del código que falla siempre
Timeout o 504Se acabó el tiempo: casi siempre, el de atrás está lentoCon mucho cuidado: suele ser saturación, y la operación pudo haberse hecho
400, 413, 415, 422La request está mal: mal formada, demasiado grande, de un tipo que no acepta o con datos inválidosNunca: va a fallar igual
401, 403Falta identificarse, o no hay permisoNo, hasta arreglar la credencial

El problema del timeout

Fíjate en la fila del timeout. Cuando una llamada vence, desde afuera no sabes cuál de estas tres cosas pasó:

  1. La request nunca llegó.
  2. Llegó y el servidor sigue trabajando, lento.
  3. El servidor ya la hizo y lo que se perdió fue la respuesta.

Las tres se ven igual: silencio. En el caso 3, reintentar repite algo que ya pasó. Si era clasificar una foto, no importa: sale la misma categoría. Si era publicar o cobrar, sale dos veces. Ese problema tiene solución, la idempotencia, y la ves en unas secciones.

Y en el caso 2, el reintento le suma trabajo a un servidor que ya está lento. La Ronda (M03) lo dice sin rodeos: un timeout casi siempre significa «el servidor está lento», no «se perdió un paquete».

C01: ningún reintento en el camino interactivo

Por eso C01 decide que, en el camino de Ana, nadie reintenta. Tiene tres razones. Un segundo intento tendría ≈ 80 ms, después de que el primero no pudo con 150. Triton probablemente sigue saturado, y el reintento le duplicaría la carga a la pieza más cara. Y el plan B, el selector de categorías, es barato y bueno. Los callers batch (programas que mandan miles de fotos sin una persona esperando) sí reintentan una vez, con espera al azar y con un límite de reintentos, como vas a ver ahora.

04

Cuánto esperar: backoff exponencial, jitter y Retry-After

Esencial

How long to wait: exponential backoff, jitter and Retry-After

Si vas a reintentar, no lo hagas de inmediato. El servidor que acaba de fallar probablemente sigue en el mismo apuro, y un reintento instantáneo le llega justo cuando menos puede.

Duplicar la espera

El backoff exponencial (exponential backoff) es esperar cada vez más entre intentos: la espera se duplica en cada reintento, hasta un tope. Con una base de 100 ms, la espera antes del reintento n es 100 × 2ⁿ ms: 200 ms antes del primero, 400 antes del segundo, 800 antes del tercero. Un tope (por ejemplo, 1 s) evita que la espera crezca sin límite.

¿Por qué duplicar? Porque no sabes cuánto va a durar el problema. Si es un tropiezo de un instante, el primer reintento lo resuelve rápido. Si es largo, la espera crece y los intentos se espacian, en vez de martillar al servidor diez veces por segundo.

El problema de la manada

El backoff tiene un hueco. Imagina que 1,000 clientes fallan en el mismo instante, porque el servidor tuvo un tropiezo de medio segundo. Si todos usan el mismo backoff, todos vuelven a los 200 ms. Juntos. Y los que fallen vuelven a los 600 ms, otra vez juntos. Esperar no separó a nadie: solo movió la ola.

La cura es el jitter: agregarle azar a la espera. La forma más usada se llama full jitter (de un artículo de Marc Brooker en el blog de arquitectura de AWS, Amazon Web Services, la nube de Amazon, de 2015): en vez de esperar exactamente 100 × 2ⁿ ms, cada cliente espera un tiempo al azar entre 0 y 100 × 2ⁿ ms. Antes del primer reintento, entre 0 y 200 ms; antes del segundo, entre 0 y 400; antes del tercero, entre 0 y 800. En promedio se espera la mitad, y los 1,000 clientes quedan repartidos en el tiempo.

Esta simulación lo muestra. Mil clientes fallan juntos en t = 0. El servidor atiende 50 requests en cada ventana de 10 ms (5,000 por segundo) y rechaza el resto. Cada cliente reintenta hasta 3 veces:

python
import heapq, random                         # heapq: una fila que siempre saca primero el más chico
from collections import Counter

def simular(jitter, clientes=1000, cupo=50, base=100, reintentos=3):
    rng = random.Random(7)                   # semilla fija: sale igual en cada corrida
    def espera(n):                           # reintento n = 1, 2, 3: tope de 200, 400, 800 ms
        tope = base * 2**n
        return rng.uniform(0, tope) if jitter else tope
    fila = [(espera(1), c, 1) for c in range(clientes)]   # todos fallaron en t = 0
    heapq.heapify(fila)
    llegan = Counter()                       # intentos que llegan en cada ventana de 10 ms
    atendidos = rendidos = 0
    while fila:
        t, c, n = heapq.heappop(fila)        # el siguiente intento en el tiempo
        ventana = int(t // 10)
        llegan[ventana] += 1
        if llegan[ventana] <= cupo:          # el servidor atiende 50 por ventana
            atendidos += 1
        elif n < reintentos:
            heapq.heappush(fila, (t + espera(n + 1), c, n + 1))
        else:
            rendidos += 1
    return atendidos, rendidos, max(llegan.values()), sum(llegan.values())

for jitter in (False, True):
    a, r, pico, total = simular(jitter)
    print(f"{'con' if jitter else 'sin'} jitter: {a} atendidos, {r} se rinden, "
          f"pico de {pico} en 10 ms, {total} intentos")
Salida
sin jitter: 150 atendidos, 850 se rinden, pico de 1000 en 10 ms, 2850 intentos
con jitter: 1000 atendidos, 0 se rinden, pico de 61 en 10 ms, 1061 intentos

Sin jitter, los 1,000 llegan en la misma ventana: el servidor atiende 50 y rechaza 950. Los 950 vuelven juntos, se atienden 50, y así. Al final, 850 clientes se rinden después de 2,850 intentos. Con jitter, los mismos 1,000 se reparten en unos 200 ms: el pico baja a 61 por ventana, casi todos entran al primer reintento y nadie se rinde. Mismo servidor, mismos clientes; solo cambió el azar en la espera.

Los reintentos de la simulación de 1,000 clientes que fallan juntos, contados en ventanas de 50 milisegundos, de 0 a 1,500 milisegundos, con la misma escala arriba y abajo. Una línea punteada marca lo que el servidor atiende en 50 milisegundos: 250. Arriba, sin jitter: tres torres, de 1,000 intentos a los 200 milisegundos, de 950 a los 600 y de 900 a los 1,400; cada una cuatro veces por encima del cupo. Hay 2,850 intentos y 850 clientes se rinden. Abajo, con jitter: cuatro barras de entre 226 y 277 intentos entre 0 y 200 milisegundos, cerca del cupo, y después casi nada. Hay 1,061 intentos y nadie se rinde. LOS MISMOS 1,000 CLIENTES, DOS ESPERAS SIN JITTER: 850 SE RINDEN 2,850 intentos en total 1,000 950 900 lo que atiende el servidor 0 500 1,000 1,500 ms CON JITTER: NADIE SE RINDE lo que atiende el servidor De 226 a 277 por ventana, cerca del cupo; 1,061 intentos; a los 600 ms ya no queda nadie 0 500 1,000 1,500 ms Cada barra: los intentos que llegan en 50 ms. Misma escala arriba y abajo; el cupo es 250.
Los reintentos de la simulación, contados en ventanas de 50 ms. Sin jitter, tres torres de 900 a 1,000 intentos, muy por encima de lo que el servidor atiende. Con jitter, cuatro ventanas cerca del cupo y luego casi nada. (El código cuenta en ventanas de 10 ms: ahí el cupo es 50 y el pico, 61.)
Cuánto espera un backoff exponencialCalculadora

Espera máxima antes del reintento n = min(tope, base × 2ⁿ). Con full jitter, cada espera sale al azar entre 0 y ese máximo. Arranca con los números de la Ronda y el deadline de 300 ms del caller de C01.

Espera máxima antes del primer reintento200 ms
Espera máxima antes del último reintentosi llega al tope, ya no crece800 ms
Espera máxima, sumando todas1.40 s
Espera promedio, sumando todas, con full jittercada espera, en promedio, es la mitad de su máximo700 ms
Parte del deadline que se come la espera máximamás de 100 %: los reintentos no caben; falta sumar lo que tarda cada intento466.7 %

Con 300 ms, ni el primer reintento cabe con holgura: por eso el camino de Ana no reintenta. Pon el deadline en 60,000 (un job batch con un minuto) y los 3 reintentos caben de sobra.

Arranca con los números de la Ronda (base de 100 ms, tope de 1 s, 3 reintentos) contra los 300 ms del caller de C01. Sube a 5 reintentos y mira cómo el tope frena el crecimiento. Después prueba una base de 15,000 ms, un tope de 120,000 y un deadline de 600,000 (10 minutos): son los reintentos al SAT de C05, en «En la vida real».

Retry-After: cuando el servidor dice cuánto esperar

A veces el servidor sabe mejor que tú cuánto esperar. Para eso existe Retry-After, un header de la respuesta que dice cuándo volver a intentar. Viene sobre todo con un 503 o un 429, y trae segundos o una fecha:

text
HTTP/1.1 429 Too Many Requests
Retry-After: 2

Esa respuesta dice «pasaste tu límite; vuelve en 2 segundos». Respetarlo es esperar al menos eso antes de reintentar. Ignorarlo es justo lo que el servidor trata de evitar: que todos vuelvan de inmediato y empeoren la saturación.

Respetarlo no quiere decir esperar a ciegas. Si el Retry-After no cabe en tu deadline, no reintentes: ríndete y usa tu plan B. En C01, en 300 ms no cabe esperar un Retry-After de 1 s, así que el camino de Ana cae al selector. Un caller batch, en cambio, sí puede esperar. El de C10, por ejemplo, ajusta cuántas requests manda a la vez: sube mientras todo sale bien y baja con cada 429 o 503, respetando su Retry-After.

Para ir más hondoOtras recetas de jitter, y el backoff de Kubernetes

Full jitter no es la única receta. Con equal jitter (jitter a medias), la mitad de la espera es fija y la otra mitad al azar: entre 100 y 200 ms antes del primer reintento, en vez de entre 0 y 200. Así nadie reintenta casi de inmediato, pero las esperas quedan menos repartidas. Con decorrelated jitter, cada espera sale al azar entre la base y el triple de la espera anterior, con un tope. En las simulaciones del artículo de Brooker, full jitter fue la que menos trabajo total le dio al servidor (decorrelated jitter terminó un poco antes, con algo más de trabajo), y por eso es la que más se recomienda.

El backoff exponencial no es solo para requests. Cuando un contenedor se cae una y otra vez al arrancar, Kubernetes lo reinicia con esperas que se duplican (10 s, 20 s, 40 s…) hasta un tope de 5 minutos, y vuelve a empezar de 10 s si el contenedor aguanta 10 minutos sin caerse. Son los valores por omisión que documenta Kubernetes 1.37 (2026); desde la 1.35, cada nodo puede bajar ese tope. El estado se llama CrashLoopBackOff, y lo vas a ver en la Ronda: es exactamente esta idea, aplicada a reiniciar.

05

La tormenta de reintentos y su presupuesto

Esencial

The retry storm and the retry budget

Los reintentos ayudan cuando las fallas son pocas y salteadas. Cuando muchas requests fallan a la vez por falta de capacidad, hacen lo contrario: multiplican la carga justo cuando el sistema tiene menos aire. Eso es una tormenta de reintentos (retry storm), la que ya viste en la lección de probabilidad como una falla que llega junta.

Cada capa multiplica

Lo peor pasa cuando varias capas reintentan, cada una por su cuenta. Piensa en una cadena de cuatro piezas: una app llama a una API, la API a un servicio y el servicio al servidor del modelo. Las tres que llaman a otra (la app, la API y el servicio) reintentan 3 veces cada una. Así, cada llamada se vuelve hasta 4 intentos (1 + 3 reintentos), y los intentos se multiplican al bajar:

Tres capas que reintentan 3 veces cada una (M03)La cuenta
Intentos que recibe la API por cada request de la appla app reintenta 3 veces
1 + 3
4
Intentos que recibe el serviciocada intento de la API hace hasta 4
4 × 4
16
Intentos que recibe el servidor del modelo
4 × 4 × 4 = 4³
64
Con el tráfico de pico de C01
500 × 64
32,000/s
Contra lo que aguantan las dos T4≈ 1,200 fotos por segundo cada una, medidas
32,000 ÷ 2,400
≈ 13 veces
Con una sola capa y presupuesto de 10 %lo ves en un momento
500 × 1.1
550/s
Resultado
Si el servidor del modelo se frena, cada request de la app puede volverse 64 intentos: 13 veces lo que aguantan las dos GPUs. Con una sola capa que reintenta y un presupuesto, la carga sube a lo mucho 10 %.
Tres capas que reintentan contra una sola con presupuesto · Arriba, cada capa reintenta por su cuenta y la carga se multiplica al bajar. Abajo, solo una capa reintenta, con presupuesto, y las demás pasan el error hacia arriba. Recorre los dos caminos.
  • App · reintenta 3
    La app manda una request y, si falla, la reintenta hasta 3 veces: a la API le llegan hasta 1 + 3 = 4 intentos. No sabe que, más abajo, cada capa los va a multiplicar.
  • API · reintenta 3
    Cada uno de esos hasta 4 intentos, si falla, lo reintenta otras 3 veces: al servicio le llegan hasta 4 × 4 = 16.
  • Servicio · reintenta 3
    Hace lo mismo con cada uno de sus hasta 16 intentos: al modelo le llegan hasta 16 × 4 = 64. Por separado, cada capa se ve razonable; nadie ve la cuenta completa.
  • Modelo · recibe hasta 64
    El servidor del modelo recibe hasta 4³ = 64 intentos por cada request de la app. Con 500 requests por segundo, 32,000: ≈ 13 veces lo que aguantan las dos T4 de C01. Así una lentitud se vuelve caída.
  • App · no reintenta
    La misma request de la app. Esta vez la app no reintenta: si le llega un error, usa su plan B.
  • API · no reintenta
    No reintenta: si recibe «sobrecargado, no reintentes», lo pasa hacia arriba tal cual. La app usa su plan B.
  • Servicio · 1 reintento
    Es la capa justo arriba de la que falla, la única que reintenta: una vez, con jitter, y solo si queda presupuesto (≤ 10 % de sus requests). Si no, se rinde y lo dice.
  • Modelo · recibe ≤ 1.1
    Recibe a lo más 1.1 intentos por request: con 500 por segundo, 550. Sigue saturado un rato, pero la carga extra no lo hunde, y en cuanto pasa el problema se recupera.

Lo traicionero es que nadie en ninguna capa ve el problema completo. Cada una cree que está siendo resiliente, porque reintentar suena a buena idea. Y la tormenta no se apaga sola: aunque la causa original desaparezca, la carga de los reintentos alcanza para mantener saturado al servidor. Cada intento fallido produce más intentos.

El presupuesto de reintentos

La primera defensa es poner pocos reintentos en una sola capa. La segunda es un presupuesto de reintentos (retry budget): un tope a cuántos reintentos puede mandar un cliente, como fracción de sus requests normales. El libro de SRE de Google (Site Reliability Engineering, el libro donde Google cuenta cómo opera sus servicios), en su capítulo sobre sobrecarga, usa dos topes juntos:

  • Por request: como mucho 3 intentos. Después de eso, la request falla.
  • Por cliente: los reintentos no pueden pasar de ≈ 10 % de las requests. Si ya se gastó el presupuesto, el cliente no reintenta: falla de una vez.

Mira la diferencia con números. El gateway manda 500 requests por segundo y, de pronto, el 20 % falla. Para reintentarlas todas mandaría 100 reintentos por segundo; el presupuesto permite 0.1 × 500 = 50. Si la falla es chica y salteada, 50 alcanzan de sobra. Si es grande, el presupuesto se acaba y la carga extra se queda en 10 %, en vez de multiplicarse.

El 10 % no es una ley: cada herramienta trae su número, y la idea es la misma: si muchas fallan, se deja de reintentar.

¿Y en qué capa se reintenta? El libro de SRE elige la que está justo arriba de la que falla. Esa capa, si se rinde, devuelve hacia arriba un error que dice «estoy sobrecargado, no reintentes» (overloaded; don't retry), para que las capas de arriba no se sumen. En C01 la decisión es todavía más simple: en el camino interactivo no reintenta nadie.

Para ir más hondoOtros presupuestos: Linkerd y gRPC

Linkerd, una malla de servicios, deja por omisión que los reintentos sumen a lo más 20 % de carga, más 10 reintentos «gratis» por segundo para los servicios con poco tráfico. gRPC tiene su propia versión, el retry throttling: un contador que baja 1 con cada falla y sube un poco con cada éxito, y que apaga los reintentos cuando cae a la mitad de su máximo.

Cuánto multiplican los reintentosCalculadora

Con L capas que reintentan r veces, al fondo le llegan hasta (1 + r)ᴸ intentos por request. Con una sola capa y un presupuesto, la carga sube a lo más ese porcentaje. Arranca con el ejemplo de M03 y el pico de C01.

Intentos por llamada en cada capael original más los reintentos4
Intentos al fondo por cada request, en el peor caso64×
Requests por segundo al fondo, si todo falla32,000
Contra lo que aguanta el fondomás de 100 %: la tormenta lo mantiene saturado1,333.3 %
Con una sola capa y el presupuestorequests por segundo, a lo más550
Contra lo que aguanta el fondo, con presupuesto22.9 %

Con 0 reintentos, la carga es la que entra. Con 1 reintento y 4 capas, ya es 16 veces. El presupuesto no depende de cuántas capas haya: por eso se reintenta en una sola.

Arranca con el ejemplo de la Ronda: 3 capas, 3 reintentos cada una y 500 requests por segundo. Baja a 1 reintento, o a 1 sola capa, y compara con lo que deja pasar el presupuesto.

Para ir más hondoPor qué la tormenta no se apaga sola

A una falla que se sostiene sola, aunque su causa ya se fue, se le llama metaestable (metastable failure). El nombre lo puso un artículo de 2021 (de Nathan Bronson y otros, en el congreso HotOS), y uno de 2022 (de Lexiang Huang y otros, en OSDI) estudió 22 casos reales en 11 organizaciones. La idea: el sistema tiene dos estados estables. En uno, todo va bien y casi nadie reintenta. En el otro, el servidor está saturado, casi todo falla, y los reintentos de esas fallas lo mantienen saturado. Un empujón (un pico, un deploy, una pausa de 2 segundos) lo pasa del primero al segundo, y quitar el empujón no lo regresa.

Para salir hay que cortar la carga: apagar los reintentos, rechazar tráfico en la entrada o abrir un circuit breaker (lo ves en dos secciones). Por eso los presupuestos y el jitter se deciden antes del incidente: a media tormenta ya es tarde para ponerlos.

06

Idempotencia: repetir sin daño

Esencial

Idempotency: repeating without harm

Volvamos al timeout ambiguo. La request de Ana pudo haberse hecho y perdido la respuesta. Si el backend reintenta «publicar», puede haber dos publicaciones de los mismos tenis. Si era un cobro, Ana paga dos veces. La pregunta de fondo es: ¿qué operaciones se pueden repetir sin daño?

Una operación es idempotente si hacerla una vez o muchas deja el mismo resultado. Eso es la idempotencia (idempotency). «Pon el volumen en 20» es idempotente: la tercera vez sigue en 20. «Sube el volumen 2 rayitas» no lo es: cada vez queda más alto. En matemáticas se escribe f(f(x)) = f(x). El valor absoluto cumple: |−5| = 5, y |5| sigue siendo 5.

Los métodos de HTTP prometen algo

En la lección de HTTP viste que cada request lleva un método. El estándar de HTTP le asigna a cada uno una promesa:

MétodoQué hace¿Idempotente?
GETLeer algoSí, y además no cambia nada
PUTDejar algo así: «el precio de la publicación 42 es 900»Sí: la segunda vez el precio ya es 900
DELETEBorrar algoSí: lo borrado sigue borrado, aunque la segunda respuesta diga «no existe»
POSTCrear algo o hacer una acción: «publica esto», «cobra esto»No: cada vez crea otra

La promesa la cumple, o no, el servidor. Lo que cuenta es lo que hace, no el nombre del método. La inferencia de Triton se pide con un POST, y aun así es idempotente: clasificar la misma foto dos veces da la misma categoría y no cambia nada en el mundo. Publicar y cobrar, en cambio, no lo son.

La llave de idempotencia

Para que un POST se pueda repetir sin daño, el cliente le pone una llave de idempotencia (idempotency key): un identificador único que genera una vez por operación, no una vez por intento, y que manda igual en cada reintento. Casi siempre es un UUID: un identificador de 128 bits, casi todos al azar, que en la práctica nunca se repite. Viaja en un header, normalmente Idempotency-Key: es el nombre que usa Stripe, una empresa de pagos en línea, y el que propone la IETF, el grupo que escribe los estándares de internet, en un borrador que en noviembre de 2026 todavía no es estándar.

El servidor guarda cada llave junto con la respuesta que dio. Si le llega una llave que ya vio, no repite la operación: devuelve la misma respuesta de antes. Para el cliente, el reintento se ve exactamente como si el primer intento hubiera funcionado.

Dos conversaciones entre un cliente y un servidor, con el tiempo hacia abajo. Arriba, sin llave: el cliente pide publicar; el servidor publica la número 1; la respuesta se pierde en la red; al cliente se le vence el timeout y reintenta; el servidor publica la número 2 y contesta que todo salió bien. Resultado: dos publicaciones de los mismos tenis. Abajo, con llave: el cliente pide publicar con la llave K; el servidor publica la número 1 y guarda que la llave K dio la número 1; la respuesta se pierde; el cliente reintenta con la misma llave K; el servidor ve que K ya está y devuelve la número 1 sin publicar otra vez. Resultado: una sola publicación. SIN LLAVE Cliente Servidor publicar publica #1 la respuesta se pierde se vence el timeout reintento: publicar publica #2 ok: #2 Resultado: dos publicaciones CON LLAVE: LA MISMA EN CADA INTENTO Cliente Servidor publicar · llave K publica #1, guarda K la respuesta se pierde se vence el timeout reintento · la misma llave K K ya está: da #1 ok: #1 Resultado: una sola publicación
Arriba, sin llave: el servidor publica, la respuesta se pierde, el cliente reintenta y el servidor publica otra vez. Abajo, con llave: el reintento lleva la misma llave, el servidor la reconoce y devuelve la respuesta que ya tenía.
python
import uuid

publicaciones = []        # lo que el servidor guardó
ya_hechas = {}            # llave de idempotencia → la respuesta que ya se dio

def publicar(titulo, llave=None):
    if llave in ya_hechas:                 # esta operación ya se hizo
        return ya_hechas[llave]            # devuelve lo mismo, sin publicar otra vez
    publicaciones.append(titulo)
    respuesta = f"publicación #{len(publicaciones)}"
    if llave:
        ya_hechas[llave] = respuesta
    return respuesta

def cliente(titulo, con_llave):
    llave = str(uuid.uuid4()) if con_llave else None   # una por operación, no por intento
    publicar(titulo, llave)                # se publica, pero la respuesta se pierde en la red
    return publicar(titulo, llave)         # el reintento lleva la misma llave

print("sin llave:", cliente("Tenis naranjas", False), "· guardadas:", publicaciones)
publicaciones.clear()
print("con llave:", cliente("Tenis naranjas", True), "· guardadas:", publicaciones)
Salida
sin llave: publicación #2 · guardadas: ['Tenis naranjas', 'Tenis naranjas']
con llave: publicación #1 · guardadas: ['Tenis naranjas']

El UUID cambia en cada corrida, y por eso el programa no lo imprime. Lo que importa es que el reintento lleva la misma llave. Si el cliente generara una nueva en cada intento, el servidor vería dos operaciones distintas y publicaría dos veces: la llave no serviría de nada.

Una advertencia de la Ronda (M03): que algo sea idempotente quiere decir que puedes reintentarlo sin efectos raros, no que te convenga. La inferencia de C01 es idempotente, y aun así no se reintenta en el camino de Ana: el problema ahí no son los duplicados, sino la carga y el tiempo.

Para ir más hondoLo que una llave de idempotencia tiene que resolver
  • Dos copias al mismo tiempo. Si el reintento llega mientras el primer intento todavía corre, el diccionario del ejemplo no basta: los dos verían «no está» y los dos publicarían. Se resuelve guardando la llave antes de hacer la operación, con un estado «en curso», en una base de datos (el programa que guarda los datos del servicio) que no acepte la misma llave dos veces. La segunda copia espera, o recibe un error de conflicto (el borrador de la IETF propone un 409, Conflict).
  • La misma llave con otros datos. Si llega la misma llave con otro precio, es un error del cliente. El servidor lo rechaza (el borrador propone un 422), en vez de devolver la respuesta vieja como si fuera de esta.
  • Cuánto guardarla. Para siempre sería caro. Se guarda unas horas o días: lo que de verdad pueden durar los reintentos. Stripe documenta (en 2026, para su API v1) que cada llave dura al menos 24 horas; después la puede borrar, y la misma llave contaría como una operación nueva.
  • Llaves que salen de los datos. A veces no hace falta generar nada. C04 usa el hash de la imagen (un resumen corto de sus bytes, que cambia si cambia un solo byte), más la versión del modelo y la configuración: el mismo trabajo da siempre la misma llave. Y C10 le da a cada uno de sus dos millones de ítems nocturnos un id estable, para que reintentar uno no lo duplique.
07

Cuando la dependencia falla: circuit breaker, load shedding, degradación y fail-open

Esencial

When a dependency fails: circuit breaker, load shedding, degradation and fail-open

Los timeouts y los reintentos sirven para fallas cortas. Pero, ¿qué hacer cuando Triton se cae entero durante minutos? Seguir llamándolo es doblemente malo. El gateway espera el timeout completo en cada request, y Triton recibe carga justo cuando intenta levantarse.

Aquí «caído» quiere decir que no contesta nada: su máquina se colgó y los mensajes no reciben respuesta, así que cada llamada espera su timeout. Si en cambio la máquina sigue viva y solo el programa murió, la conexión se rechaza al instante y el gateway contesta de inmediato un 503 con Retry-After, como pide C01. El breaker sirve sobre todo en el primer caso.

El circuit breaker

Un circuit breaker («interruptor de circuito»; la idea la popularizó Michael Nygard en su libro Release It!, de 2007) es un contador dentro del cliente (en C01 sería una biblioteca dentro del gateway, no un servicio aparte) que vigila las fallas de una dependencia. Si son demasiadas, deja de llamarla un rato y contesta de inmediato con el plan B. Tiene tres estados. Los números son los del ejemplo de M15, un punto de partida y no una receta:

  • Cerrado (closed): todo normal. Deja pasar las llamadas y cuenta las fallas y los timeouts. Si en los últimos 10 s fallan más del 50 %, con al menos 20 llamadas, se abre.
  • Abierto (open): no llama. Cada request recibe el plan B al instante, sin tocar la red. Así se queda 30 s.
  • Semiabierto (half-open): deja pasar unas pocas llamadas de prueba, 5. Si salen bien, se cierra; si fallan, vuelve a abrirse otros 30 s.

El mínimo de 20 llamadas existe para la madrugada: sin él, 2 fallas de 3 requests a las 4 a. m. serían «67 % de fallas» y abrirían el breaker por nada.

Los tres estados de un circuit breaker, con los números de ejemplo de M15. Cerrado: llama a la dependencia y cuenta las fallas. Si fallan más del 50 por ciento en los últimos 10 segundos, con al menos 20 llamadas, pasa a abierto. Abierto: no llama, y cada request recibe el plan B en un milisegundo. Cuando pasan 30 segundos, pasa a semiabierto. Semiabierto: deja pasar 5 llamadas de prueba. Si salen bien, vuelve a cerrado; si fallan, vuelve a abierto otros 30 segundos. El mínimo de 20 llamadas evita que dos fallas de tres, de madrugada, lo abran por nada. TRES ESTADOS, CON LOS NÚMEROS DE M15 Cerrado llama y cuenta las fallas más de 50 % de fallas en 10 s, con ≥ 20 llamadas Abierto no llama: plan B en ≈ 1 ms pasan 30 s Semiabierto deja pasar 5 de prueba si fallan si salen bien El mínimo de 20 llamadas evita que 2 fallas de 3, a las 4 a. m., abran el breaker por nada.
Los tres estados del breaker y lo que hace pasar de uno a otro, con los números de M15. Cerrado deja pasar todo; abierto contesta el plan B sin llamar; semiabierto prueba con 5.
C01 con Triton caído entero, sin y con breaker (M15)La cuenta
Sin breaker: requests esperando a TritonLittle: cada una espera los 150 ms
500 × 0.15 s
75
Lo que tarda el caller en saberun 504 por el timeout
150 ms + lo del gateway
≈ 150–160 ms
Con el breaker abierto: en vuelocada una dura ≈ 1 ms
500 × 0.001 s
0.5
Lo que tarda el caller en saberun 503 con el plan B, sin red
—
≈ 1 ms
Requests que no tocan a Triton en 30 s abierto
500 × 30
15,000
Resultado
El breaker no salva ninguna request, porque Triton está caído igual. Pero libera al gateway, le da al caller su plan B 150 veces más rápido y deja a Triton arrancar sin carga.

C01 no trae breaker en su versión de referencia: lo cubren el timeout y el health check del gateway, que lo saca del balanceador cuando Triton cae. La Ronda lo marca como la mejora natural.

Load shedding: rechazar rápido

Cuando llega más de lo que se puede atender, algo tiene que ceder. Lo peor es encolar sin límite: la fila crece, la espera crece con ella y, al final, cada request vence su deadline adentro de la fila. El load shedding («soltar carga») es lo contrario: rechazar de inmediato lo que no vas a poder atender a tiempo, con un 503. Rechazar al entrar cuesta microsegundos; atender una request que ya no llega a tiempo cuesta todo su trabajo, para nada.

C01 lo hace en dos lugares. El gateway, con MAX_INFLIGHT = 16 por pod: la request 17 recibe un 503 al instante. Y la fila de Triton, que rechaza lo que esperó más de 100 ms. Es una forma de backpressure, avisar hacia atrás que no das abasto, con una fila con tope. Y cuando hay que soltar, se suelta primero lo que no tiene a una persona esperando: los callers batch antes que la app de Ana.

Degradación: dar menos, pero dar algo

La degradación (graceful degradation, degradarse con gracia) es responder con algo más simple cuando la pieza buena falla, en vez de un error. En C01, sin sugerencia, Ana elige la categoría a mano en el selector: el fallback de la lección del camino completo. Publicar sigue funcionando, con un paso más.

Un buen plan B tiene dos condiciones: es más barato que el camino normal y no depende de lo que falló. Y se mide: C01 cuenta cada respuesta de plan B por motivo (timeout, 503, foto inválida). Un fallback que se usa en silencio esconde una caída; con su métrica y su alerta, se nota.

¿Abierto o cerrado?

Hay piezas que no dan una respuesta, sino un permiso: un revisor de contenido, un limitador de requests. Si el revisor falla, hay que haber decidido antes qué pasa. Fallar abierto (fail-open) es dejar pasar; fallar cerrado (fail-closed), bloquear. Se elige según qué cuesta más: dejar pasar algo malo un rato, o detener todo lo bueno.

En C06, la moderación de un sitio de publicaciones, si los modelos no contestan a tiempo, el post se publica y se revisa después: bloquear toda publicación porque una GPU tarda sería una caída del producto. En un pago o en un inicio de sesión se elige lo contrario: si no se puede verificar, no pasa.

Ojo con las palabras: en fail-open, «abierto» deja pasar; en el breaker, «abierto» no deja pasar. Una es una puerta; el otro, un circuito eléctrico.

Para ir más hondoFallar abierto, pero con cuidado

Fallar abierto no es apagar la revisión y olvidarse. En C06, lo único que falla cerrado es la comparación contra una lista de imágenes prohibidas conocidas, porque corre dentro del mismo programa y no depende de la red: nunca se apaga. Lo que se publicó sin revisar entra a la revisión de después con prioridad alta, y la tasa de fail-open tiene su métrica y su alerta, como la tasa de fallback de C01.

Lo mismo pasa con un limitador de requests que guarda sus cuentas en otro servidor. Si ese servidor no contesta en ≈ 5 ms, el gateway no se cae ni deja pasar todo: aplica un límite local, más conservador, y sigue. Es fallar abierto, pero con un tope (M15).

08

Hedging: pedir dos veces para recortar la cola

Extra

Hedging: asking twice to cut the tail

Todo lo anterior trata de fallas. El hedging trata de otra cosa: de las requests que no fallan, pero tardan muchísimo. En las lecciones de percentiles y de probabilidad viste la idea: si una request no ha respondido cuando ya pasó lo que tarda su p95, mandas una copia a otra réplica, te quedas con la primera respuesta que llegue y cancelas la otra. El nombre viene de to hedge, cubrirse: como quien le apuesta a dos caballos para no perderlo todo.

Un reintento espera a que algo falle. El hedging no espera: apuesta a que la lentitud es mala suerte de una réplica, y que otra, al mismo tiempo, no la tiene.

¿Por qué disparar en el p95 y no antes? Por lo que cuesta. Si mandaras la copia desde el principio, a todas las requests, duplicarías la carga para rescatar a unas pocas. Esperar al p95 quiere decir que solo 5 de cada 100 requests llegan a ese punto sin respuesta, así que solo esas pagan una copia. Disparar en el p99 costaría ≈ 1 % de carga, pero rescataría menos y más tarde. El p95 es un punto medio común, no una ley: se ajusta mirando cuánta carga extra aguanta el servicio.

Los números de M03

La Ronda pone un ejemplo: el 2 % de las requests cae en una réplica que se pausa ≈ 1 s, al azar y por su lado (por ejemplo, porque el programa se detiene a limpiar su memoria). Sin hedging, ese 2 % es más que el 1 % que deja afuera el p99, así que el p99 queda en ≈ 1 s.

Hedging a los 45 ms, el p95 (M03)La cuenta
Requests que pasan del disparoes el p95: 5 de cada 100
—
≈ 5 %
Carga extrauna copia por cada una
—
≈ +5 %
Siguen lentasel original y la copia caen en pausa, cada una por su lado
0.02 × 0.02
0.04 %
Lo más que tarda una rescatadadisparo + lo que tarda la copia, ≈ 40 ms
45 + 40
≈ 85 ms
Resultado
Con 0.04 % de lentas, menos del 1 %, el p99 ya no cae en una pausa: baja de ≈ 1 s a ≈ 85 ms o menos, por ≈ 5 % más de carga.

Esta simulación lo comprueba con 100,000 requests. Lo normal tarda ≈ 30 ms; el 2 % cae en una pausa de ≈ 1 s:

python
import random
rng = random.Random(3)

def intento():                        # una llamada a una réplica, en ms
    if rng.random() < 0.02:           # el 2 % cae en una réplica que se pausa ≈ 1 s
        return 1000 + rng.uniform(0, 50)
    return max(10, rng.gauss(30, 8))  # lo normal: ≈ 30 ms

def percentil(datos, p):
    orden = sorted(datos)
    return orden[int(p / 100 * len(orden)) - 1]

N, DISPARO = 100_000, 45              # requests; el hedge sale a los 45 ms
sin, con, copias = [], [], 0
for _ in range(N):
    t = intento()
    sin.append(t)
    if t > DISPARO:                   # no respondió a tiempo: va una copia a otra réplica
        copias += 1
        t = min(t, DISPARO + intento())   # gana la primera que llegue
    con.append(t)

print(f"p95 sin hedging: {percentil(sin, 95):.0f} ms")
print(f"p99 sin hedging: {percentil(sin, 99):.0f} ms · con hedging: {percentil(con, 99):.0f} ms")
print(f"más de 500 ms: {sum(t > 500 for t in sin) / N:.2%} sin · {sum(t > 500 for t in con) / N:.2%} con")
print(f"copias extra: {copias / N:.1%} de las requests")
Salida
p95 sin hedging: 45 ms
p99 sin hedging: 1024 ms · con hedging: 75 ms
más de 500 ms: 1.98% sin · 0.04% con
copias extra: 4.9% de las requests

El p99 baja de ≈ 1 s a 75 ms, y solo el 0.04 % sigue lento, como dice la cuenta. Sale un poco abajo de los 85 de la Ronda porque aquí la copia tarda ≈ 30 ms, no 40.

Cuándo no

El hedging funciona por una sola razón: la lentitud de las dos réplicas es independiente. Si no lo es, solo agrega carga. La Ronda lista cuándo no usarlo:

  • Inferencia cara en una GPU cerca de su límite. El 5 % extra cae en la pieza más cara, y si la lentitud viene de saturación, la empeora. Por eso C01 no hace hedging contra Triton.
  • Lentitud compartida. Si la request tarda porque la foto es enorme, la copia tarda igual.
  • Operaciones que no son idempotentes. Dos copias que se ejecutan son dos efectos.
  • Sin cancelación. Si no puedes cancelar la copia que pierde, pagas el trabajo doble.

Donde encaja es en lecturas baratas que ya tienen réplicas: buscar en un índice repartido en varias máquinas o leer de un caché. En el artículo que lo hizo famoso, The Tail at Scale, Google lo usó en lecturas así, como viste en la lección de percentiles.

C07 es justo ese caso. Parte su índice de 100 millones de vectores en 4 pedazos (shards), cada uno con dos copias, y cada búsqueda pregunta a los 4 y espera a todos. Es el fan-out de la lección de probabilidad: basta con que un pedazo tenga su mala racha para que toda la búsqueda tarde. Por eso, si un pedazo no ha respondido en su p95, C07 le pide lo mismo a su otra copia. Y si de plano no llega a su timeout, contesta con los 3 que sí llegaron: una respuesta parcial, otra forma de degradarse.

09

Errores comunes

Esencial

Common mistakes

1. Una llamada sin timeout, o un timeout de adentro más largo que el de afuera

Una llamada sin límite, o un gateway que le da 5 s a Triton cuando el caller se va a los 300 ms. Las requests se apilan para respuestas que nadie lee, y un servicio lento se vuelve uno caído.

Cómo evitarlo: un timeout explícito en cada llamada que sale, y cada salto con min(su tope, lo que queda del deadline). Revisa la cadena completa: cada número más chico que el de arriba.

2. Reintentar en todas las capas

Cada capa agrega «sus» 3 reintentos, y tres capas dan 64 intentos por request. Nadie lo ve, porque cada capa por separado se ve razonable.

Cómo evitarlo: reintentos en una sola capa, con presupuesto (≈ 10 %), y que esa capa pase hacia arriba un error de «no reintentes». Dibuja la cadena de llamadas y cuenta los intentos al fondo.

3. Reintentar sin esperar, o esperar todos lo mismo

Un reintento instantáneo le pega al servidor en el mismo apuro. Un backoff sin jitter mueve la ola en vez de repartirla: mil clientes vuelven en el mismo milisegundo.

Cómo evitarlo: backoff exponencial con tope y full jitter, y respetar Retry-After cuando viene.

4. Reintentar lo que no se puede repetir

Un POST de publicar o de cobrar que se reintenta después de un timeout: el primer intento sí se hizo y solo se perdió la respuesta. Ana paga dos veces.

Cómo evitarlo: reintentar solo lo idempotente, o con una llave de idempotencia generada una vez por operación y repetida en cada intento.

5. Reintentar errores permanentes

Un 400 o un 422 se reintenta tres veces y falla tres veces. Peor: un trabajo que falla siempre, como una foto corrupta, se reintenta sin fin y ocupa workers.

Cómo evitarlo: clasificar la falla antes de reintentar: conexión rechazada, 503 y 429 sí; los demás 4xx, no. Ponerle un tope a los intentos de cada trabajo y apartar los que lo pasan.

6. Un plan B que nadie ve, o que nunca se probó

El fallback funciona tan bien que nadie nota que la mitad de las requests lo usa desde hace una semana. O el día que hace falta, descubres que dependía de lo mismo que falló.

Cómo evitarlo: contar cada fallback por motivo, con alerta, y ejercitarlo a propósito de vez en cuando.

10

En la vida real

Extra

In real life

C01: todas las piezas juntas

El camino de Ana tiene las reglas de esta lección. La cadena de deadlines 300 → 250 → 150 → 100 ms, para que cada salto se rinda antes que el de arriba. Ningún reintento en el camino interactivo, porque el plan B es bueno y barato; los callers batch reintentan una vez, con jitter, respetando Retry-After y con un presupuesto de ≤ 10 % de las requests. Load shedding con MAX_INFLIGHT = 16 y la fila de Triton con su tope de 100 ms. Y el selector de categorías como degradación, con su métrica. La Ronda lo resume así: sin timeouts, un Triton lento tumba el servicio entero; con timeouts cortos y load shedding, el sistema se degrada, algunas requests reciben 503 y el resto sigue.

C05: reintentos al SAT con jitter completo

C05 lee facturas y verifica cada una contra el SAT, el servicio de impuestos de México, que a veces está lento o caído. Cada factura hace hasta 3 reintentos con jitter completo, con esperas al azar de hasta 30, 60 y 120 s:

Los reintentos al SAT de C05 (M15)La cuenta
Lo más que espera una factura
30 + 60 + 120 s
210 s ≈ 3.5 min
Lo que espera en promediocon full jitter, la mitad de cada tope
(30 + 60 + 120) ÷ 2
105 s ≈ 1.75 min
Resultado
Después de los 3 reintentos, la factura va a revisión marcada «SAT no verificado». Una persona espera minutos sin problema; por eso aquí sí se reintenta.

La Ronda propone una mejora: durante una caída de horas, miles de facturas esperarían sus reintentos una por una. Con un breaker compartido, las nuevas saltarían directo a «no verificado», y el SAT no recibiría una ola de reintentos al volver.

C10: un LLM no se reintenta a medio camino

C10 sirve un LLM que escribe su respuesta token por token, en streaming. Un solo timeout total no sirve: una respuesta larga y sana puede tardar un minuto (1,024 tokens a ≈ 50 ms cada uno son ≈ 51 s). Por eso usa un timeout por fase: conectar, 2 s; primer evento, 10 s; silencio máximo entre eventos, 30 s; request completa, 120 s.

Y no se reintenta a mitad del stream. Si la falla llega antes del primer byte, todavía se puede convertir en un error limpio o en otro proveedor. Si llega con 120 tokens ya en la pantalla, pegar en silencio una segunda respuesta daría un texto que no tiene sentido. Lo honesto es avisar del error y ofrecer regenerar.

C12: el reembolso que se paga dos veces

C12 es un agente de soporte: un LLM que, para ayudar, llama herramientas, como «reembolsar este pedido». Si una herramienta da timeout y el orquestador (el programa que lleva los pasos del agente) reintenta sin llave de idempotencia, el reembolso se paga dos veces. Por eso cada herramienta con efectos recibe una llave, y cada una lleva su propio timeout.

C06: publicar aunque el revisor no conteste

C06 modera posts y comentarios antes de publicarlos, con modelos en Triton y la promesa de contestar en 150 ms (su p99). Su gateway se da un presupuesto propio de 120 ms: si a esa altura falta la calificación de algún modelo, decide con lo que tiene, falla abierto y publica, y el post queda marcado para revisarse después con prioridad. Así responde a tiempo aunque Triton esté lento. Lo que nunca se apaga es la comparación contra imágenes prohibidas conocidas, que corre dentro del proceso: esa falla cerrado. Y la tasa de fail-open tiene su alerta, porque una que sube en silencio es una moderación que dejó de moderar.

Fuera de ML: pagar en línea

Cuando una tienda en línea te dice «no presiones dos veces el botón de pagar», está confesando que su cobro no es idempotente. Las pasarelas de pago modernas, como Stripe, aceptan una llave de idempotencia en cada cobro: si la app reintenta con la misma llave, el cobro se hace una sola vez. Es exactamente el ejemplo de esta lección, con dinero de verdad.

Y el backoff con jitter lo inventas tú cada vez que una línea está ocupada. Si mil personas llaman a un programa de radio para ganar boletos y todas vuelven a marcar exactamente al minuto, la línea sigue ocupada para casi todas. Las que esperan un rato al azar, y cada vez un poco más, tienen más suerte.

11

Ejercicios

Esencial

Exercises

Resuélvelos con lápiz antes de abrir la solución. Los de backoff y de amplificación se pueden comprobar con las calculadoras.

  1. 1Fácil

    Para cada caso, ¿reintentarías? (a) Conexión rechazada. (b) Un 422. (c) Un 429 con Retry-After: 2, desde un job batch. (d) El mismo 429, en el camino de Ana, que se rinde a los 300 ms. (e) Un 413.

    Ver la solución paso a paso
    • (a) Sí, mejor contra otra réplica: no había nadie escuchando y la request nunca llegó.
    • (b) No: los datos están mal y van a estar mal la segunda vez.
    • (c) Sí, después de al menos 2 s, y dentro del presupuesto.
    • (d) No: 2 s no caben en 300 ms. Se rinde y usa el selector.
    • (e) No: la request es demasiado grande y lo seguirá siendo.
  2. 2Fácil

    Un backoff exponencial con base de 50 ms y tope de 1 s hace 5 reintentos. ¿Cuál es la espera máxima antes de cada uno? ¿Cuánto espera en total, como máximo y en promedio con full jitter?

    Ver la solución paso a paso
    • Antes del reintento n: 50 × 2ⁿ ms, con tope de 1,000: 100, 200, 400, 800 y 1,000 ms (el quinto daría 1,600, pero el tope lo recorta).
    • Máximo total: 100 + 200 + 400 + 800 + 1,000 = 2,500 ms.
    • Con full jitter, cada espera es al azar entre 0 y su tope: en promedio, la mitad. 1,250 ms.
  3. 3Fácil

    ¿Cuáles son idempotentes? (a) GET de las categorías. (b) PUT que fija el precio de la publicación 42 en 900. (c) DELETE de la publicación 42. (d) POST que crea una publicación. (e) POST a Triton para clasificar una foto.

    Ver la solución paso a paso
    • (a) Sí: leer no cambia nada.
    • (b) Sí: la segunda vez el precio ya es 900.
    • (c) Sí: lo borrado sigue borrado, aunque la segunda respuesta diga que no existe.
    • (d) No: cada vez crea otra. Hace falta una llave de idempotencia.
    • (e) Sí, aunque sea un POST: la misma foto da la misma categoría y no cambia nada. Lo que cuenta es el efecto, no el método.
  4. 4Fácil

    ¿Cuántos intentos le llegan al fondo, en el peor caso, con (a) 2 capas que reintentan 2 veces y (b) 4 capas que reintentan 1 vez? ¿Cuántos por segundo con las 500 requests de C01?

    Ver la solución paso a paso
    • (a) Cada llamada se vuelve 1 + 2 = 3 intentos: 3² = 9, y 500 × 9 = 4,500 por segundo.
    • (b) 1 + 1 = 2 por capa: 2⁴ = 16, y 500 × 16 = 8,000 por segundo. Un solo reintento por capa parece inofensivo; con 4 capas, multiplica por 16.
  5. 5Medio

    El deadline del gateway es de 250 ms y el tope de la llamada a Triton, 150 ms. ¿Qué timeout le toca a la llamada si recibir y preparar la foto tomó (a) 70 ms y (b) 160 ms? En (b), ¿quién se rinde primero, el gateway o la fila de Triton, que rechaza a los 100 ms?

    Ver la solución paso a paso
    • (a) Quedan 250 − 70 = 180 ms: min(150, 180) = 150 ms.
    • (b) Quedan 250 − 160 = 90 ms: min(150, 90) = 90 ms.
    • En (b), el gateway se rinde a los 90 ms, antes que los 100 de la fila. Si la request sigue en la fila, es trabajo zombi, y por eso conviene que el gateway cancele hacia abajo.
  6. 6Medio

    Alguien sube el timeout del gateway hacia Triton a 400 ms «para dar margen». Triton se traba con las 500 requests por segundo de C01, repartidas en 5 pods con MAX_INFLIGHT = 16, y el resto del trabajo de cada request sigue tomando ≈ 40 ms. ¿Cuántas requests quedan en vuelo por pod? ¿Qué parte se rechaza con 503? ¿Qué ve el caller?

    Ver la solución paso a paso
    • Por pod llegan 100 por segundo, y cada request dura ahora ≈ 40 + 400 = 440 ms. Little: 100 × 0.44 = 44 en vuelo, pero solo hay 16 lugares.
    • Cada lugar se libera a los 440 ms: el pod despacha 16 ÷ 0.44 ≈ 36 por segundo. Se rechazan ≈ 64 de 100: ≈ 64 %.
    • Las que entran esperan ≈ 440 ms, pero el caller se fue a los 300: su respuesta llega cuando nadie la espera. Con 150 ms, todas tenían su respuesta a tiempo. Subir el timeout no dio margen: lo quitó.
  7. 7Medio

    Triton se cae entero a las 12:00:00, a 500 requests por segundo. El breaker se abre con más de 50 % de fallas en los últimos 10 s (con al menos 20 llamadas); antes de la caída todo iba bien. (a) ¿Cuándo se abre, más o menos? (b) ¿Cuántas requests esperaron sus 150 ms antes de eso? (c) ¿Cuántas no tocan a Triton en los 30 s que está abierto? (d) ¿Qué pasa a las 12:00:35, si Triton sigue caído?

    Ver la solución paso a paso
    • (a) Las fallas pasan del 50 % de la ventana cuando la mitad de los últimos 10 s ya es de caída: ≈ 5 s después, a las 12:00:05.
    • (b) 500 × 5 = ≈ 2,500 requests, cada una con su 504 a los ≈ 150 ms.
    • (c) 500 × 30 = 15,000, cada una con su plan B en ≈ 1 ms.
    • (d) A las 12:00:35 pasa a semiabierto y deja pasar 5 llamadas de prueba. Fallan, y se abre otros 30 s.
  8. 8Reto

    Un servicio de búsqueda tiene réplicas de un índice. El 3 % de sus lecturas cae en una réplica que se pausa ≈ 800 ms, al azar; su p95 es de 50 ms y una copia tarda ≈ 40 ms. (a) Sin hedging, ¿dónde queda su p99? (b) Con hedging en el p95, ¿qué fracción sigue lenta, cuánto tarda a lo más una rescatada y cuánta carga extra cuesta? (c) Otro equipo propone lo mismo para una GPU de inferencia al 90 % de uso. ¿Qué le dices?

    Ver la solución paso a paso
    • (a) El 3 % lento es más que el 1 % que deja afuera el p99: el p99 cae en una pausa, ≈ 800 ms.
    • (b) Siguen lentas 0.03 × 0.03 = 0.09 %, menos del 1 %. Una rescatada tarda a lo más 50 + 40 = ≈ 90 ms, y ese es el nuevo p99, más o menos. Cuesta ≈ 5 % de carga: las que pasan del p95.
    • (c) Que no. El 5 % extra la sube a ≈ 0.9 × 1.05 ≈ 94.5 % de uso, más cerca de su límite, en la pieza más cara. Y si su lentitud viene de saturación, la copia es igual de lenta: el hedging solo suma carga.
  9. 9Reto

    Diseña la herramienta «reembolsar pedido» del agente de C12. El orquestador la llama con un timeout de 5 s y puede reintentar. (a) ¿Quién genera la llave y cuándo? (b) Un reintento llega mientras el primer intento sigue corriendo: ¿qué debe pasar? (c) Llega la misma llave con otro monto: ¿qué respondes? (d) ¿Cuánto tiempo guardas cada llave?

    Ver la solución paso a paso
    • (a) El orquestador, una vez por reembolso decidido, antes del primer intento, y la manda igual en cada reintento. Si generara una por intento, cada reintento sería otro reembolso.
    • (b) El servidor guarda la llave antes de pagar, con estado «en curso», en una base que no acepta la misma llave dos veces. La segunda copia no paga: espera el resultado o recibe un error de conflicto y vuelve a preguntar.
    • (c) Un error: la misma operación no puede traer otro monto. Devolver el resultado viejo escondería un bug del cliente.
    • (d) Lo que puedan durar los reintentos, con margen: horas o días. Stripe, por ejemplo, documenta al menos 24 horas. Guardarla para siempre cuesta espacio y no protege de nada más.
12

Lo que sigue

Esencial

What comes next

En la Ronda:

  • M02 · Patrones de inferencia, en «Entrega, reintentos e idempotencia»: el backoff con jitter, los 4³ = 64 intentos y la llave que el cliente genera una vez por trabajo.
  • M03 · Latencia y capacidad, en «Timeouts, reintentos y hedging»: la jerarquía de timeouts, el presupuesto de 10 %, el hedging en el p95 y el load shedding.
  • M15 · Seguridad y confiabilidad, en «Degradación»: la cadena 300 → 250 → 150 → 100 ms, el circuit breaker y el plan B de cada caso, del selector de C01 al fail-open de C06.
  • Los casos. C01 (sin reintentos interactivos), C05 (reintentos al SAT), C10 (timeouts por fase) y C12 (llaves en las herramientas).

En el Dojo: la estación 6, «Código de serving», escribe el gateway de C01 con su timeout hacia Triton y su MAX_INFLIGHT, y la 11, «La request en vivo», la sigue cuando algo falla. Cuando abran, ahí lo vas a practicar.

En Sensei: la siguiente lección, «Qué tan rápido y cuánto: latencia y throughput», explica por qué la fila se dispara cerca del 100 % de uso: la razón de fondo para rechazar rápido. La lección «Colas y streams: mensajes que esperan» retoma la idempotencia: una cola que entrega cada mensaje al menos una vez obliga a aguantar duplicados.

13

Palabras que vas a oír

Esencial

Words you will hear

Estos son los conceptos que presentó esta lección. Cada uno tiene su tarjeta con la definición completa.

TimeouttimeoutEl tiempo máximo que un programa espera algo (una respuesta, una conexión) antes de rendirse. Cuando vence, deja de esperar, cancela y lo trata como una falla.Más

Sin timeout, una llamada a un servicio lento puede esperar para siempre, y mientras espera ocupa memoria, una conexión y un lugar en vuelo. Una llamada por la red suele tener tres: el de conexión (que la otra máquina acepte el handshake), el de lectura (el silencio máximo entre pedazos de la respuesta) y el total (la llamada completa, el único que acota todo). Por la Ley de Little, el timeout acota cuántas requests se juntan cuando la pieza de abajo se traba. Cada salto debe rendirse antes que quien lo llamó: por eso los timeouts salen de un deadline.

Analogía. Esperar a un amigo en un café con la regla «si a las 5:15 no llegó, me voy». Se rompe en que, al irte, el amigo no se entera: en la red, si el que espera no cancela, el de atrás puede seguir trabajando para nadie.

Ejemplo. El gateway de C01 espera a Triton como máximo 150 ms (INFER_TIMEOUT_S=0.15). Si Triton se traba a 500 requests por segundo, quedan 500 × 0.15 = 75 llamadas colgadas; con un timeout de 5 s serían 2,500.

Ver en el glosario →

DeadlinedeadlineEl momento en que una request entera deja de importar. Cada programa por el que pasa (cada salto) calcula cuánto le queda y espera como máximo eso: min(su propio tope, lo que queda del deadline).Más

Un timeout es una duración por llamada; un deadline es una fecha límite para toda la request, que viaja con ella. Así ningún salto espera más de lo que le queda a quien lo llamó, y no hay trabajo zombi: servidores calculando respuestas que nadie va a leer. Con gRPC, el deadline viaja solo en el header grpc-timeout; con HTTP lo calcula cada programa. Entre máquinas se manda lo que queda (una duración), no la hora límite, porque sus relojes no coinciden exactamente.

Ejemplo. La cadena de C01: el caller se rinde a los 300 ms; el gateway, a los 250 (para alcanzar a contestar un 504); la llamada a Triton tiene como tope 150 ms, y la fila de Triton rechaza a los 100. Si preparar la foto tomó 140 ms, a la llamada le toca min(150, 110) = 110 ms.

Ver en el glosario →

ReintentoretryVolver a mandar una request que falló, esperando que la segunda vez salga bien. Sirve para fallas pasajeras (503, 429, conexión rechazada) y nunca para errores de la request (400, 422).Más

Si cada intento falla al azar el 1 % de las veces, dos intentos fallan juntos el 0.01 %: es una réplica en el tiempo. Pero solo si los intentos son independientes: si el servidor está caído o saturado, el segundo cae en el mismo estado y le suma carga. Un reintento bien hecho espera con backoff y jitter, respeta Retry-After, cabe en el deadline, se hace en una sola capa con presupuesto y solo repite lo idempotente. Después de un timeout es delicado: la operación pudo haberse hecho y solo se perdió la respuesta.

Ejemplo. En el camino interactivo de C01 nadie reintenta: un segundo intento tendría ≈ 80 ms y el selector manual es mejor respuesta. Los callers batch reintentan una vez, con jitter y con un presupuesto de ≤ 10 % de las requests.

Ver en el glosario →

Backoff exponencialexponential backoffEsperar cada vez más entre reintentos: la espera se duplica en cada uno (200, 400, 800 ms…), hasta un tope.Más

Un reintento inmediato le llega al servidor justo cuando sigue en apuros. Duplicar la espera resuelve rápido los tropiezos cortos y espacia los intentos cuando el problema es largo, en vez de martillar. Con base b, la espera antes del reintento n es b × 2ⁿ, recortada por un tope. Solo, no basta: clientes que fallaron juntos vuelven juntos. Por eso va siempre con jitter. Kubernetes usa la misma idea para reiniciar contenedores que se caen al arrancar (CrashLoopBackOff).

Ejemplo. Con base de 100 ms y tope de 1 s, 5 reintentos esperan como máximo 200, 400, 800, 1,000 y 1,000 ms: 3,400 ms en total. Con full jitter, en promedio la mitad: 1,700 ms.

Ver en el glosario →

JitterjitterAzar que se le agrega a la espera antes de un reintento, para que los clientes que fallaron juntos no vuelvan juntos. Con «full jitter», cada uno espera al azar entre 0 y el tope del backoff.Más

Si mil clientes fallan en el mismo instante y todos esperan exactamente lo mismo, vuelven en el mismo milisegundo: el backoff solo movió la ola. Con jitter, la ola se vuelve llovizna. La receta más usada, full jitter (de un artículo de Marc Brooker en el blog de AWS, de 2015), espera un tiempo uniforme entre 0 y b × 2ⁿ. Otras: equal jitter (mitad fija, mitad al azar) y decorrelated jitter. Es la primera defensa contra una tormenta de reintentos.

Ejemplo. En la simulación de la lección, 1,000 clientes fallan juntos contra un servidor que atiende 50 por ventana de 10 ms. Sin jitter: pico de 1,000 en una ventana, 2,850 intentos y 850 se rinden. Con full jitter: pico de 61, 1,061 intentos y todos atendidos.

Ver en el glosario →

Retry-AfterRetry-After headerUn header de la respuesta (un dato con nombre que viaja junto con ella) en el que el servidor dice cuándo volver a intentar: un número de segundos o una fecha. Viene sobre todo con un 503 o un 429.Más

Es el servidor diciéndole al cliente cuánto esperar, en vez de que el cliente lo adivine con su backoff. Respetarlo es esperar al menos eso antes de reintentar; ignorarlo es lo que empeora una saturación, porque todos vuelven de inmediato. Si lo que pide no cabe en el deadline, no se reintenta: se usa el fallback. Es una forma de backpressure: el servidor avisa hacia atrás que no da abasto.

Ejemplo. HTTP/1.1 429 Too Many Requests con Retry-After: 2 quiere decir «vuelve en 2 segundos». Un job batch espera y reintenta; el camino de Ana en C01, que se rinde a los 300 ms, no puede esperar 2 s y muestra el selector.

Ver en el glosario →

Tormenta de reintentosretry stormCuando los reintentos multiplican la carga justo cuando falta capacidad: un servidor lento recibe cada vez más intentos, y la carga de reintentos lo mantiene saturado aunque la causa original ya se haya ido.Más

Pasa sobre todo cuando varias capas reintentan por su cuenta: con L capas que reintentan r veces, al fondo le llegan hasta (1 + r)ᴸ intentos por request. Nadie la ve completa, porque cada capa cree que está siendo resiliente. Es una falla correlacionada: los reintentos de miles de clientes llegan juntos, no al azar. Se previene con reintentos en una sola capa, jitter, un presupuesto y, para salir de ella, cortando carga: load shedding o un circuit breaker.

Ejemplo. Tres capas que reintentan 3 veces: 4 × 4 × 4 = 64 intentos al fondo por request. Con las 500 requests por segundo de C01 serían 32,000 por segundo, ≈ 13 veces lo que aguantan sus dos T4.

Ver en el glosario →

Presupuesto de reintentosretry budgetUn tope a cuántos reintentos puede mandar un cliente, como fracción de sus requests normales: ≈ 10 % en el libro de SRE de Google, 20 % por omisión en Linkerd. Si ya se gastó, no se reintenta: se falla de una vez.Más

Un límite por request (por ejemplo, 3 intentos) no basta: si todo falla, cada request gasta sus 3. El presupuesto acota la carga extra total: con ≈ 10 %, en el peor caso el servidor recibe 1.1 veces su tráfico, no 4 ni 64. El libro de SRE de Google usa los dos topes juntos y pide reintentar en una sola capa, la que está justo arriba de la que falla, que hacia arriba devuelve «sobrecargado, no reintentes». Es la defensa contra la tormenta de reintentos.

Ejemplo. El gateway manda 500 requests por segundo y falla el 20 %. Para reintentarlas todas harían falta 100 reintentos por segundo; el presupuesto de 10 % permite 50, y la carga sube a lo más a 550 por segundo.

Ver en el glosario →

IdempotenciaidempotencyUna operación es idempotente si hacerla una vez o muchas deja el mismo resultado. Es lo que permite reintentar sin miedo a duplicar: clasificar una foto lo es; publicar o cobrar, no.Más

Después de un timeout no sabes si la request nunca llegó, si sigue en curso o si ya se hizo y se perdió la respuesta. Si la operación es idempotente, repetirla no importa. En HTTP, GET, PUT y DELETE prometen serlo; POST no. Pero lo que cuenta es lo que hace el servidor: la inferencia de Triton se pide con POST y es idempotente. Para lo que no lo es, se usa una llave de idempotencia. Idempotente quiere decir que se puede reintentar, no que convenga.

Analogía. El botón del elevador: apretarlo cinco veces trae un solo elevador. Se rompe en que el elevador sabe que todos los botonazos son la misma petición; en la red, el servidor no sabe si dos requests iguales son la misma intención o dos distintas.

Ejemplo. «Pon el precio en 900» (PUT) es idempotente: la segunda vez ya está en 900. «Publica estos tenis» (POST) no: un reintento después de una respuesta perdida deja dos publicaciones.

Ver en el glosario →

Llave de idempotenciaidempotency keyUn identificador único que el cliente genera una vez por operación y repite en cada reintento. El servidor guarda la llave con su respuesta y, si la vuelve a ver, devuelve lo mismo sin repetir la operación.Más

Vuelve idempotente un POST. Casi siempre es un UUID (un identificador de 128 bits, casi todos al azar) en el header Idempotency-Key, el nombre que usa Stripe y que propone un borrador de la IETF; a veces sale de los datos mismos, como un hash del contenido más la versión del modelo. Tiene que ser una por operación lógica: si el cliente genera una nueva en cada intento, no sirve. El servidor la guarda antes de hacer la operación, con un estado «en curso», para que dos copias simultáneas no pasen las dos; rechaza la misma llave con otros datos, y la guarda unas horas o días.

Ejemplo. El agente de C12 llama a la herramienta «reembolsar». Si la herramienta da timeout y el orquestador reintenta con la misma llave, el reembolso se paga una vez. Sin llave, dos. Stripe documenta (en 2026, para su API v1) que cada llave dura al menos 24 horas antes de que la pueda borrar.

Ver en el glosario →

Circuit breakercircuit breakerUn contador dentro del cliente que, si un servicio del que depende falla demasiado, deja de llamarlo un rato y contesta de inmediato con el plan B. Tiene tres estados: cerrado, abierto y semiabierto.Más

Cerrado: deja pasar las llamadas y cuenta fallas y timeouts. Abierto: no llama; cada request recibe el fallback al instante. Semiabierto: deja pasar unas pocas llamadas de prueba; si salen bien se cierra, si no vuelve a abrirse. No salva requests, pero libera los recursos del cliente, da el plan B mucho más rápido y deja que la dependencia se recupere sin carga, lo que corta una tormenta de reintentos. Lleva un mínimo de llamadas para no abrirse por dos fallas de madrugada. La idea la popularizó Michael Nygard en su libro Release It! (2007).

Analogía. La pastilla del tablero eléctrico, que corta la corriente si pasa demasiada. Se rompe en que la pastilla salta con un solo pico y espera a que alguien la suba; el breaker de software cuenta una tasa y se prueba solo (semiabierto).

Ejemplo. Los números de ejemplo de M15: se abre con más de 50 % de fallas en los últimos 10 s y al menos 20 llamadas; se queda abierto 30 s; semiabierto, prueba con 5. Con Triton caído entero y sin contestar, baja de 75 requests esperando a Triton a 0.5 en vuelo, y el plan B llega en ≈ 1 ms en vez de ≈ 150.

Ver en el glosario →

Degradacióngraceful degradationResponder con algo más simple cuando la pieza buena falla, en vez de un error: el producto sigue funcionando, aunque peor.Más

Es la decisión de producto detrás del fallback: qué se da cuando el modelo no está. Un buen plan B es más barato que el camino normal y no depende de lo que falló. Se mide (cada fallback por motivo, con alerta), porque uno que se usa en silencio esconde una caída, y se ejercita a propósito, porque uno que nunca se usó puede fallar justo cuando hace falta. Muchos casos tienen una escalera de degradación: varios escalones, cada uno un poco peor y más barato que el anterior.

Ejemplo. En C01, si no hay sugerencia, Ana elige la categoría a mano en el selector: publicar sigue funcionando, con un paso más. En C10, cuando la flota de GPUs no alcanza, primero se pausa el trabajo batch, después se recorta lo que hace cada request y, más abajo en la escalera, una parte se manda a un proveedor externo.

Ver en el glosario →

Load sheddingload sheddingRechazar de inmediato, con un 503, lo que no se va a poder atender a tiempo, en vez de encolarlo. Rechazar cuesta microsegundos; atender lo que ya no llega a tiempo cuesta todo su trabajo para nada.Más

Cuando llega más de lo que cabe, encolar sin límite es lo peor: la fila crece y cada request vence su deadline adentro. Soltar carga mantiene rápido lo que sí entra y le da al caller su fallback pronto. Se suelta primero lo que no tiene una persona esperando. Va de la mano con la backpressure (avisar hacia atrás que no das abasto) y con las filas con tope.

Ejemplo. C01 lo hace en dos lugares: el gateway, con MAX_INFLIGHT = 16 por pod (la request 17 recibe un 503 al instante), y la fila de Triton, que rechaza lo que esperó más de 100 ms.

Ver en el glosario →

Hedginghedging · hedged requestsSi una request no respondió cuando ya pasó su p95 (lo que tardan 95 de cada 100), mandar una copia a otra réplica, quedarse con la primera respuesta y cancelar la otra. Recorta la cola por ≈ 5 % más de carga.Más

Un reintento espera a que algo falle; el hedging apuesta a que la lentitud es mala suerte de una réplica. Solo funciona si la lentitud de las réplicas es independiente (una pausa al azar), si la operación es idempotente y si se puede cancelar la copia que pierde. No conviene en inferencia cara cerca de su límite, donde el 5 % extra cae en la pieza más cara, ni cuando la lentitud es compartida (la misma foto gigante). Encaja en lecturas baratas y replicadas. Lo describe el artículo The Tail at Scale, de Jeffrey Dean y Luiz André Barroso, de Google (2013).

Ejemplo. Si el 2 % de las requests cae en una pausa de ≈ 1 s, el p99 es ≈ 1 s. Con hedging a los 45 ms (el p95), siguen lentas 0.02 × 0.02 = 0.04 %, y el p99 baja a ≈ 85 ms o menos, por ≈ 5 % más de carga.

Ver en el glosario →

Fail-open (fallar abierto)fail-open · fail-closedCuando una revisión que da permiso falla, dejar pasar. Lo contrario, fail-closed (fallar cerrado), es bloquear. Se decide de antemano, según qué cuesta más.Más

Aplica a piezas que no dan una respuesta sino un permiso: un revisor de contenido, un limitador, un chequeo de fraude. Fallar abierto protege la disponibilidad del producto a cambio de dejar pasar algo malo un rato; fallar cerrado protege la seguridad a cambio de detener todo. Bien hecho, fallar abierto tiene tope, métrica y alerta, y lo que pasó sin revisar se revisa después. Ojo: en el circuit breaker, «abierto» quiere decir lo contrario, que no deja pasar.

Ejemplo. En C06, si los modelos de moderación no contestan a tiempo, el post se publica y se revisa después (abierto); la comparación contra imágenes prohibidas conocidas, que no depende de la red, falla cerrado. En un pago, si no se puede verificar, no pasa (cerrado).

Ver en el glosario →

14

Chequeo

Esencial

Check

Intenta responder en tu cabeza antes de abrir cada respuesta.

  1. 1¿Por qué un servicio lento es peor que uno caído?Ver respuesta

    El caído falla rápido y quien llama pasa a su plan B. El lento retiene memoria, conexiones y lugares en vuelo hasta que todo se atasca. El timeout lo convierte en uno que falla rápido.

  2. 2¿Qué diferencia hay entre un timeout y un deadline?Ver respuesta

    El timeout es cuánto espera una llamada. El deadline es el momento en que la request entera deja de importar. Cada salto usa min(su tope, lo que queda del deadline), y así nadie espera más que quien lo llamó.

  3. 3¿Por qué el backoff exponencial necesita jitter?Ver respuesta

    Porque los clientes que fallaron juntos, con el mismo backoff, vuelven juntos. El azar en la espera los reparte en el tiempo, y el servidor recibe una llovizna en vez de una ola.

  4. 4Tres capas reintentan 3 veces cada una. ¿Cuántos intentos le llegan al fondo, y cómo se evita?Ver respuesta

    4³ = 64. Se evita reintentando en una sola capa, con un presupuesto de ≈ 10 % de las requests, y pasando hacia arriba un error de «no reintentes».

  5. 5¿Qué hace un circuit breaker abierto, y para qué sirve si no salva ninguna request?Ver respuesta

    Contesta el plan B al instante, sin llamar. Libera al cliente, da el plan B mucho más rápido y deja que la dependencia se recupere sin carga.

  6. 6En C01, ¿por qué el camino de Ana no reintenta, si la inferencia es idempotente?Ver respuesta

    Idempotente quiere decir que se puede, no que convenga. El reintento tendría ≈ 80 ms y le duplicaría la carga a un Triton saturado. El selector es mejor respuesta.

Sigue · 26Qué tan rápido y cuánto: latencia y throughputLa red y la web Léelo en la RondaM02 · Patrones de inferenciaLo que acabas de leer, con el ojo del entrevistador. ← 24 · Balanceadores, proxies y gateways