Un escenario común
Una tienda online consulta a un servicio externo para calcular el costo de envío durante la compra. Un día, ese servicio no se cae: simplemente empieza a tardar 60 segundos en responder. Cada cliente que llega al checkout ocupa una conexión del servidor esperando. En pocos minutos, todas las conexiones están ocupadas y la tienda deja de responder, incluso para clientes que solo querían ver el catálogo.
El servicio de envíos tuvo un problema; la tienda sufrió una caída completa. Esto se conoce como falla en cascada, y casi siempre se origina en llamadas externas sin límites.
1. Tiempos de espera: la primera defensa
Muchas librerías de HTTP esperan indefinidamente o durante minutos si no se configura un límite. Toda llamada externa debe tener tiempos de espera explícitos:
- Tiempo de conexión: cuánto esperar para establecer la conexión. Suele ser corto, porque si el servidor no acepta la conexión rápido, es poco probable que lo haga después.
- Tiempo de respuesta: cuánto esperar la respuesta una vez conectados. Se define según el comportamiento normal del servicio y lo que el usuario puede tolerar.
- Plazo total de la operación: si hay reintentos, la suma de todos los intentos también necesita un límite.
Una forma práctica de elegir el valor es observar cuánto tarda normalmente el servicio en los casos lentos (por ejemplo, el percentil 99) y dejar un margen razonable, sin superar lo que la experiencia del usuario permite.
2. Reintentos: útiles, pero peligrosos
Muchas fallas son transitorias y un segundo intento funciona. Pero los reintentos mal diseñados multiplican la carga sobre un servicio que ya tiene problemas y pueden empeorar la situación.
| Situación | ¿Reintentar? | Comentario |
|---|---|---|
| Tiempo de espera agotado | Solo si la operación es idempotente | No se sabe si la operación se ejecutó. |
| Error de conexión | Sí, con espera | Probablemente la solicitud no llegó. |
| Error 5xx del servidor | Sí, con espera y límite | Puede ser transitorio. |
| 429 (demasiadas solicitudes) | Sí, respetando el tiempo indicado | Si el servicio indica cuánto esperar, hay que respetarlo. |
| Error 4xx de validación | No | Reintentar la misma solicitud dará el mismo error. |
| 401 / 403 | No | Problema de credenciales o permisos. |
Backoff exponencial con jitter
Entre intentos hay que esperar, y cada espera debe ser mayor que la anterior (backoff exponencial). Además, conviene agregar un componente aleatorio (jitter) para que miles de clientes no reintenten exactamente al mismo tiempo.
llamarConReintentos(operacion, maxIntentos = 3, base = 200ms, tope = 5s):
para intento en 1..maxIntentos:
resultado = operacion(timeout = 2s)
si resultado.exitoso o no esReintentable(resultado):
devolver resultado
si intento == maxIntentos:
devolver resultado
espera = aleatorio(0, min(tope, base * 2^intento)) // "full jitter"
dormir(espera)
Si una solicitud atraviesa tres capas y cada una reintenta tres veces, un solo error en el fondo puede generar hasta 27 llamadas. Reintenta en una sola capa, preferiblemente la más cercana al servicio que falla.
Reintentar solo lo que es seguro
Una consulta se puede repetir sin consecuencias. Crear un pedido, cobrar o enviar un mensaje, no. Para reintentar operaciones que modifican datos, el servicio debe admitir una clave de idempotencia, o tu sistema debe verificar el resultado antes de repetir. Lo explicamos aplicado a pagos en webhooks de pago e idempotencia.
3. Circuit breaker: dejar de insistir
Si un servicio está claramente caído, seguir llamándolo solo consume recursos y retrasa la respuesta al usuario. Un circuit breaker (interruptor) observa los resultados y, cuando los errores superan un umbral, deja de llamar durante un tiempo y falla de inmediato.
| Estado | Comportamiento | Transición |
|---|---|---|
| Cerrado | Las llamadas pasan normalmente y se cuentan los errores. | Si los errores superan el umbral, pasa a abierto. |
| Abierto | Las llamadas fallan de inmediato sin contactar al servicio. | Tras un tiempo de espera, pasa a semiabierto. |
| Semiabierto | Se permiten unas pocas llamadas de prueba. | Si funcionan, vuelve a cerrado; si fallan, vuelve a abierto. |
Fallar rápido parece contradictorio, pero protege al resto del sistema y permite mostrar al usuario un mensaje claro en lugar de una pantalla que carga indefinidamente.
4. Aislamiento: que un problema no consuma todo
Si todas las llamadas externas comparten el mismo grupo de conexiones o hilos, un único servicio lento puede agotarlo. El patrón de compartimentos asigna recursos separados a cada dependencia: si el servicio de envíos se vuelve lento, solo se agotan sus conexiones, y el catálogo y los pagos siguen funcionando.
5. Degradación controlada
La pregunta clave de diseño es: ¿qué debe hacer el sistema si este servicio no está disponible? Algunas respuestas habituales:
- Usar el último valor conocido desde una caché, cuando un dato ligeramente desactualizado es aceptable (tipos de cambio, catálogos).
- Diferir la operación: guardar la tarea en una cola y completarla cuando el servicio se recupere (envío de correos, registro en un ERP).
- Desactivar una función secundaria y mantener el resto: por ejemplo, ocultar recomendaciones si su servicio falla.
- Informar al usuario cuando no hay alternativa, con un mensaje claro y sin perder lo que ya ingresó.
Inventar un resultado favorable cuando el servicio no responde. Un pago, una validación de identidad o un control de stock nunca deben asumirse exitosos por defecto.
6. Pasar a asíncrono cuando sea posible
Muchas llamadas externas no necesitan completarse mientras el usuario espera. Registrar una venta en el ERP o enviar una notificación pueden hacerse en segundo plano. Una técnica útil es guardar en la misma transacción de base de datos el cambio de negocio y un registro de «mensaje pendiente» (patrón outbox), que luego un proceso envía con reintentos. Así no se pierde el mensaje si el servicio externo está caído en ese momento.
7. Medir cada dependencia
Ninguno de estos mecanismos sirve si no sabes cuándo se activan. Para cada servicio externo conviene medir cantidad de llamadas, porcentaje de errores, tiempos de respuesta, reintentos y aperturas del circuit breaker. Profundizamos en esto en observabilidad en integraciones.
Errores frecuentes
- Usar los tiempos de espera por defecto de la librería sin revisarlos.
- Reintentar errores de validación o de autenticación.
- Reintentar sin espera o con esperas fijas idénticas para todos los clientes.
- Reintentar operaciones de cobro sin idempotencia.
- Configurar reintentos en varias capas a la vez.
- No definir qué hace el sistema cuando una dependencia no está disponible.
Lista de verificación
- Cada llamada externa tiene tiempo de conexión, de respuesta y plazo total.
- Los reintentos son limitados, con backoff y jitter, y solo para errores reintentables.
- Las operaciones que modifican datos se reintentan solo con idempotencia.
- Las dependencias críticas tienen circuit breaker y recursos aislados.
- Está definido el comportamiento degradado de cada dependencia.
- Cada dependencia tiene métricas y alertas propias.
Preguntas frecuentes
¿Cuántos reintentos son adecuados?
Para llamadas que ocurren mientras el usuario espera, pocos (por ejemplo, dos o tres) y con un plazo total corto. Para procesos en segundo plano se puede reintentar más veces y durante más tiempo, siempre con límite y con un destino para los mensajes que agotan los intentos.
¿Necesito una librería especial para el circuit breaker?
No necesariamente, pero la mayoría de lenguajes tienen librerías probadas para reintentos y circuit breakers. Suele ser preferible usarlas antes que implementar la lógica desde cero.
¿El proveedor no debería encargarse de su disponibilidad?
Sí, pero ningún servicio está disponible el 100 % del tiempo. La resiliencia de tu sistema frente a sus fallas es responsabilidad de tu diseño.