La resiliencia es la práctica deliberada de limitar hasta dónde puede propagarse una falla a través de un sistema, en lugar de asumir —o esperar— que cada dependencia descendente siempre responderá correctamente y a tiempo.
La sección de resiliencia cubre un conjunto específico de patrones: tiempos de espera, reintentos con retroceso exponencial, disyuntores y degradación elegante.
Leídos individualmente, pueden parecer una caja de herramientas arbitraria; leídos como un modelo, son en realidad una secuencia de preguntas que una solicitud hace sobre una dependencia con problemas, y cada patrón responde a la pregunta que el anterior no pudo.
Conceptos Básicos de Resiliencia muestra código funcional para cada patrón; esta página es el razonamiento que los conecta y explica por qué el orden en que se aplican es tan importante como cuáles eliges.
Los patrones de resiliencia forman una secuencia en capas que contiene el radio de explosión de una falla —limitando cuánto tiempo esperas, si reintentas, cuándo dejas de intentarlo y qué haces sin la dependencia— en lugar de prevenir la falla en sí.
Por Qué Es Importante: En un backend distribuido, la pregunta no es si una dependencia eventualmente fallará o se ralentizará, sino si tu propio servicio sigue comportándose de manera predecible cuando eso sucede.
Conceptos Clave:dominio de falla, tiempo de espera, retroceso exponencial, disyuntor, mamparo, degradación elegante.
Cuándo Usar: Al diseñar cualquier llamada saliente a una API de terceros o servicio interno, dimensionar pools de trabajadores/conexiones, elegir una política de reintentos y decidir qué hace una característica cuando una dependencia no crítica no está disponible.
Limitaciones / Compensaciones: Cada capa añade latencia a la ruta de falla y código para mantener; reintentar contra una dependencia con problemas sin cuidado puede empeorar una interrupción en lugar de mejorarla (una tormenta de reintentos).
Temas Relacionados: tiempos de espera en todas partes, reintentos con retroceso exponencial, disyuntores, degradación elegante.
Un dominio de falla es el límite dentro del cual se contiene una falla, el conjunto de cosas que se rompen juntas cuando una cosa se rompe.
Sin un diseño deliberado, los dominios de falla de un backend de Node son mucho más grandes de lo que deberían ser: una sola consulta lenta a la base de datos puede agotar tu pool de conexiones, lo que detiene todas las demás solicitudes que usan ese pool, lo que se propaga hasta que tu servicio falla por completo; un mal día de una dependencia se convierte en un mal día para todo tu proceso.
Los patrones de resiliencia existen para reducir los dominios de falla al tamaño del problema real.
Una analogía útil son los mamparos estancos de un barco: una brecha en el casco en un compartimento inunda ese compartimento y el compartimento contiguo, pero las puertas entre secciones están construidas específicamente para que el barco en su conjunto se mantenga a flote en lugar de que una sola perforación lo hunda por completo.
Cada patrón de resiliencia en esta sección es un tipo diferente de mamparo, que responde a una pregunta diferente en lo que en realidad es una secuencia fija:
¿Cuánto tiempo espero antes de rendirme en esta llamada? - tiempos de espera.
Dado que falló, ¿debería intentarlo de nuevo? - reintentos con retroceso exponencial.
Dado que sigue fallando, ¿debería dejar de intentarlo por completo por un tiempo? - disyuntores.
¿Cuántas de estas llamadas pueden estar en curso antes de que una dependencia lenta agote toda mi capacidad? - mamparos.
Dado que esta dependencia no está realmente disponible en este momento, ¿qué experimenta el usuario en su lugar? - degradación elegante.
Omitir una capa anterior no solo elimina esa protección, sino que socava las capas posteriores: los reintentos sin un tiempo de espera pueden reintentar una llamada que todavía está pendiente del primer intento, y un disyuntor sin un mamparo aún puede quedarse sin capacidad debido a que las llamadas concurrentes se acumulan antes de que se active.
La secuencia anterior no es solo una lista, es un flujo de control, y verla como un diagrama aclara por qué existe cada capa:
solicitud
│
▼
┌─────────────┐ excede el límite ┌──────────────┐
│ tiempo de │ ───────────────────▶ │ falla/reintento │
│ espera │ └──────┬───────┘
└─────────────┘ │ reintentos agotados, o
│ la tasa de error supera el umbral
▼
┌──────────────┐
│ disyuntor │ abierto: falla rápida,
│ │ no se intentan llamadas
└──────┬───────┘
│ disyuntor abierto, o
│ dependencia no crítica
▼
┌──────────────┐
│ degradación │
│ elegante │ sirve una respuesta
└──────────────┘ reducida pero funcional
La máquina de estados interna de un disyuntor es el mecanismo que vale la pena definir con precisión, porque su estado intermedio es la parte que la gente suele entender mal.
type BreakerState = "closed" | "open" | "half-open";// closed: las llamadas pasan normalmente; se cuentan las fallas// open: las llamadas fallan inmediatamente, ninguna solicitud llega a la dependencia// half-open: se permite exactamente una llamada de prueba, para verificar la recuperación// - éxito -> cerrado de nuevo; falla -> abierto de nuevo, el enfriamiento se reinicia
Cerrado es el estado normal: las llamadas pasan y las fallas simplemente se cuentan para un umbral.
Abierto es el estado de falla contenida: el disyuntor falla cada llamada inmediatamente, sin siquiera intentar la solicitud de red, que es lo que realmente protege la capacidad de tu propio servicio (hilos, sockets, tiempo del bucle de eventos) de ser consumida por llamadas a algo que ya se sabe que está caído.
Semiabierto existe porque un disyuntor que simplemente vuelve a cerrarse después de un período de enfriamiento fijo corre el riesgo de golpear una dependencia apenas recuperada con todo tu tráfico a la vez; permitir que pase exactamente una (o un número pequeño y controlado de) llamadas de prueba primero responde "¿se ha recuperado realmente?" antes de volver a comprometer el resto de tu tráfico.
Aquí es también donde los reintentos y los disyuntores interactúan de una manera que es fácil de entender al revés: un bucle de reintentos que sigue reintentando contra un circuito abierto es un trabajo inútil, ya que el disyuntor ya está fallando rápidamente por diseño; la lógica de reintentos debe verificar el estado del disyuntor, no reintentar ciegamente debajo de él.
El retroceso exponencial —esperar progresivamente más tiempo entre los intentos de reintento, generalmente con un jitter aleatorio añadido— existe para evitar que cada cliente fallido reintente exactamente en el mismo momento en que una dependencia con problemas está tratando de recuperarse, lo que de otro modo convertiría un breve problema en una ola sincronizada de carga renovada (una tormenta de reintentos) justo cuando la dependencia menos puede manejarla.
A escala, el mayor modo de falla contra el que los patrones de resiliencia se protegen no es una sola dependencia que se cae, sino la falla en cascada, donde una ralentización en un servicio se propaga a través de cadenas de llamadas síncronas hasta que una parte no relacionada del sistema también falla, puramente porque estaba esperando algo que esperaba algo más.
Los mamparos (que limitan las llamadas concurrentes a una dependencia específica, a través de un semáforo, un pool de conexiones dedicado o un límite de trabajadores) existen específicamente para detener esta propagación en el punto de contacto: un proveedor lento puede agotar su propia capacidad asignada sin afectar la capacidad reservada para todo lo demás que hace tu servicio.
La ingeniería del caos —inyectar deliberadamente fallas (instancias eliminadas, latencia añadida, conexiones caídas) en un sistema para observar si sus patrones de resiliencia se comportan realmente como se diseñaron— existe porque estos patrones son notoriamente difíciles de verificar leyendo solo el código; el umbral de un disyuntor, la duración de un tiempo de espera y la curva de retroceso de un reintento interactúan de maneras que son mucho más fáciles de equivocarse en el papel que de observar bajo una falla inyectada.
La observabilidad no es opcional aquí: cada capa de resiliencia necesita su propia señal para ser útil operativamente —una tasa creciente de tiempos de espera, las transiciones de abierto/semiabierto de un disyuntor, un recuento creciente de reintentos— porque sin ellas, un patrón de resiliencia que hace su trabajo en silencio (contener una falla) puede parecer idéntico desde el exterior a un patrón de resiliencia que falla en silencio para ayudar en absoluto.
El lugar donde reside esta lógica también ha cambiado a lo largo de la historia de la industria: los primeros servicios de Node implementaban bucles de reintentos manualmente; bibliotecas como opossum y cockatiel estandarizaron la lógica de disyuntores y reintentos como componentes reutilizables y probables; y cada vez más, las mallas de servicios (Envoy, Istio) implementan tiempos de espera, reintentos y disyuntores en la capa de infraestructura, completamente fuera del código de la aplicación, lo que intercambia la flexibilidad a nivel de aplicación por una consistencia impuesta en cada servicio, independientemente del lenguaje.
Dónde Reside la Lógica de Resiliencia
Fortaleza
Debilidad
Mejor Ajuste
Código de aplicación en línea
Control total; sin nueva dependencia de infraestructura
Fácil de equivocarse sutilmente; inconsistente entre servicios/equipos
Servicios pequeños, o lógica que necesita matices específicos del negocio
Biblioteca (opossum, cockatiel)
Semántica de fallas probada, reutilizable, documentada
Todavía configurada y propiedad por servicio; puede variar entre servicios
La mayoría de los servicios de Node: la opción predeterminada
Malla de servicios / sidecar
Política consistente en cada servicio, agnóstico al lenguaje
Infraestructura real para ejecutar y operar; menos control granular por llamada
Arquitecturas grandes de múltiples servicios y políglotas
"Reintentar una solicitud fallida siempre es la opción más segura." Reintentar una operación no idempotente (un cargo de pago, un POST no idempotente) puede ejecutarla dos veces; los reintentos solo son seguros cuando la operación es idempotente o está protegida por una clave de idempotencia.
"Un disyuntor repara la dependencia que falla." Solo contiene el radio de explosión de tu lado al fallar rápidamente en lugar de acumular llamadas contra algo que ya está teniendo problemas; la dependencia en sí misma todavía necesita su propia solución o recuperación.
"Los tiempos de espera por sí solos son suficiente resiliencia para las llamadas salientes." Un tiempo de espera evita que una llamada se quede colgada para siempre, pero no dice nada sobre si reintentar, cuántas llamadas concurrentes están permitidas o qué ve el usuario cuando se alcanza el tiempo de espera; las otras capas existen porque un tiempo de espera por sí solo deja esas preguntas sin respuesta.
"Los patrones de resiliencia solo son relevantes para los microservicios." Incluso un monolito llama a una base de datos, una caché y, a menudo, a API de terceros; cada una de ellas es una dependencia que puede ser lenta o no estar disponible, independientemente de cuántos servicios estés ejecutando.
"La degradación elegante significa que la característica falla silenciosamente." Bien hecha, la degradación es una alternativa deliberada y visible (un valor en caché, una respuesta simplificada, un estado claro de "temporalmente no disponible), no una característica que devuelve datos incorrectos o vacíos en silencio sin que nadie se dé cuenta.
¿Qué significa realmente "resiliencia" para un backend de Node.js?
La práctica deliberada de limitar hasta dónde se propaga una falla a través de tu sistema —a través de tiempos de espera, reintentos, disyuntores, mamparos y degradación elegante— en lugar de asumir que cada dependencia siempre responderá correctamente y a tiempo.
¿Por qué estos patrones deben aplicarse en un orden específico?
Porque cada uno responde a una pregunta que el anterior deja abierta: un tiempo de espera limita cuánto tiempo esperas, un reintento decide si volver a intentarlo, un disyuntor decide cuándo dejar de intentarlo por completo, y la degradación elegante decide qué ve el usuario cuando nada funcionó. Omitir una capa anterior socava las posteriores; reintentar sin un tiempo de espera, por ejemplo, puede reintentar una llamada que todavía está pendiente.
¿Cómo funciona realmente el estado semiabierto de un disyuntor?
Después de un período de enfriamiento en el estado abierto, el disyuntor permite que pase un número pequeño y controlado de llamadas de prueba en lugar de reanudar el tráfico completo inmediatamente. Si esas llamadas de prueba tienen éxito, el disyuntor se cierra y el tráfico normal se reanuda; si fallan, se reabre y el enfriamiento se reinicia; esto evita golpear una dependencia apenas recuperada con la carga completa de una sola vez.
¿Por qué es peligroso reintentar una solicitud no idempotente?
Porque no puedes saber, a partir de un tiempo de espera o una conexión caída, si la solicitud original realmente tuvo éxito en el servidor antes de que se perdiera la respuesta; reintentarla podría ejecutar la misma operación (como un cargo de pago) una segunda vez. Los reintentos solo son seguros cuando la operación es idempotente o está protegida por una clave de idempotencia explícita que el servidor puede desduplicar.
¿Cuál es la diferencia real entre un disyuntor y un mamparo?
Un disyuntor decide si intentar una llamada, basándose en el historial de fallas recientes de esa dependencia. Un mamparo limita cuántas llamadas pueden estar en curso a la vez a una dependencia dada, independientemente de si tienen éxito; así, un mamparo protege tu propia capacidad incluso de una dependencia que es simplemente lenta en lugar de fallar por completo.
¿Por qué el retroceso exponencial necesita un jitter aleatorio, y no solo un retraso creciente?
Sin jitter, cada cliente que falló en el mismo momento reintenta en el mismo momento nuevamente, creando una ola sincronizada de carga (una tormenta de reintentos) exactamente cuando una dependencia en recuperación menos puede manejarla. El jitter aleatorio distribuye esos reintentos en el tiempo, suavizando la carga en lugar de concentrarla.
¿Es un patrón de resiliencia innecesario para una llamada dada?
Sí, la resiliencia siempre intercambia algo de latencia y complejidad de código añadidas por contención, por lo que una llamada a una dependencia interna genuinamente confiable y de bajo riesgo con un acoplamiento estrecho a su llamador puede no necesitar la secuencia completa. La decisión es sobre qué modos de falla son plausibles y lo suficientemente costosos como para valer la pena protegerse, no aplicar cada patrón uniformemente en todas partes.
¿Cómo se diferencia una falla en cascada de la caída de una sola dependencia?
Una falla de una sola dependencia se contiene si los patrones de resiliencia funcionan; una falla en cascada es lo que sucede cuando esa contención no se mantiene, y una ralentización en un servicio se propaga a través de cadenas de llamadas síncronas hasta que partes no relacionadas del sistema también fallan, puramente por esperar algo que está esperando algo más.
¿Por qué la observabilidad se describe como parte del modelo de resiliencia en lugar de una preocupación separada?
Porque una capa de resiliencia que funciona correctamente (conteniendo una falla en silencio) y una que falla en silencio para ayudar pueden parecer idénticas desde el exterior sin señales dedicadas —una tasa de tiempo de espera, las transiciones de estado de un disyuntor, un recuento de reintentos— para distinguirlas. Sin esas señales, no puedes verificar que los patrones estén haciendo su trabajo.
¿Cuál es la compensación de mover la lógica de resiliencia a una malla de servicios en lugar de al código de la aplicación?
Una malla de servicios (como Envoy o Istio) impone una política consistente de tiempo de espera, reintentos y disyuntores en cada servicio, independientemente del lenguaje, a costa de ejecutar y operar infraestructura real y perder parte del control granular y específico del negocio que permite la lógica de aplicación en línea.
¿La ingeniería del caos reemplaza la necesidad de razonar cuidadosamente sobre estos patrones?
No, es una técnica de verificación, no de diseño. Inyectar deliberadamente fallas (instancias eliminadas, latencia añadida) prueba si tus duraciones de tiempo de espera, políticas de reintentos y umbrales de disyuntores se comportan realmente como se pretende juntos, pero aún tienes que razonar a través de la secuencia e interacciones para diseñarlos en primer lugar.
¿Cuál es un ejemplo simple de degradación elegante bien hecha versus mal hecha?
Bien hecha: una página de producto recurre a una lista de recomendaciones en caché, con una nota visible, cuando el servicio de recomendaciones en vivo está caído. Mal hecha: la misma página omite silenciosamente las recomendaciones sin ninguna indicación de que algo anda mal, dejando a los usuarios y a los ingenieros de guardia sin saber que una dependencia ha fallado.