Circuit breakers y timeouts en Node.js (guía 2026)
Cuando tu aplicación llama a otra API por HTTP, hereda el peor día de esa API. Si la dependencia se vuelve lenta, cada request que le mandás se acumula, retiene una conexión y termina arrastrando tu servicio al piso. La solución son dos patrones viejos y poco glamorosos: circuit breakers y timeouts agresivos. Acá te muestro cómo implementar ambos en Node.js sin framework.
Un timeout es un límite de tiempo que le ponés a una request: si la respuesta no llega en X milisegundos, cortás y liberás la conexión. Un circuit breaker es un patrón de resiliencia con tres estados (CLOSED, OPEN, HALF_OPEN) que cuenta los fallos de una dependencia y, al cruzar un umbral, deja de llamarla y falla al instante hasta que se recupera. Juntos evitan que un solo servicio lento derrumbe todo el sistema.
En 30 segundos
- El incidente más común en producción es la falta de timeout: la mayoría de los clientes HTTP esperan para siempre por defecto.
- Un timeout protege una request (ponele, 2 segundos de techo). Si la dependencia no contestó, fallás rápido y liberás la conexión.
- Un circuit breaker protege todo el sistema: trackea fallos repetidos y “abre” el circuito para dejar de golpear un servicio que ya está caído.
- Los tres estados del breaker son CLOSED, OPEN y HALF_OPEN. El último deja pasar una request de prueba para ver si el servicio volvió.
- El combo real es breaker + timeout: el breaker envuelve la request con timeout, así una dependencia lenta cuenta como fallo y eventualmente abre el circuito.
¿Qué pasa cuando una API externa se pone lenta?
Ponele que tu app llama a una API de pagos para mostrar el estado de una orden. Un día esa API empieza a tardar 30 segundos en responder en vez de 200 milisegundos. ¿Qué pasa con tu servicio? Se cuelga, aunque tu código esté perfecto.
El motivo es el connection pool. Cada request que sale a esa API lenta retiene una conexión mientras espera. Como las respuestas no llegan, las conexiones no se liberan, se acumulan los pedidos nuevos, y en cuestión de segundos agotás el pool. A partir de ahí ni siquiera podés atender requests que no tienen nada que ver con la API de pagos. Una dependencia se cayó y te llevó puesto a vos también. Tema relacionado: en tus flujos de despliegue automatizado.
Es como una sola caja lenta en el supermercado un sábado a la tarde: la cola crece, la gente que venía a comprar otra cosa queda atrapada, y todo el local se traba por un solo punto. El servidor donde corre tu app (sea un VPS en donweb.com o lo que uses) no tiene magia para arreglar eso: si no le ponés límites, espera.
¿Qué son los timeouts y por qué son obligatorios?
Un timeout es un techo duro de tiempo. Si la dependencia no contestó en, digamos, 2 segundos, cortás la request, liberás la conexión y devolvés un error en vez de dejar todo colgado.
Acá viene el dato que más duele: por defecto, la mayoría de los clientes HTTP esperan para siempre. No hay límite. Una sola dependencia estancada alcanza para agotar tu connection pool. Por eso el timeout faltante es, según la fuente original, el incidente de producción más común. No es un detalle de optimización. Es lo primero que tenés que poner.
La diferencia es simple: con timeout fallás rápido (fail fast) y seguís atendiendo el resto. Sin timeout, esperás eternamente y te caés.
¿Cómo implementar timeouts en Node.js sin framework?
En Node moderno no necesitás librerías. AbortSignal.timeout() te arma la señal de cancelación y se la pasás a fetch. Si se vence, la request se aborta sola. Lo explicamos a fondo en herramientas modernas de CI/CD.
async function fetchConTimeout(url, ms = 2000) {
const signal = AbortSignal.timeout(ms);
const res = await fetch(url, { signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}Eso te da un techo duro. Si la dependencia no respondió en 2 segundos, fallás rápido y liberás la conexión en lugar de dejarla colgada. Fijate que el timeout protege una sola request: si la API está caída del todo, cada intento igual te va a costar esos 2 segundos antes de fallar. Ahí entra el segundo patrón.
¿Qué son los circuit breakers y en qué se diferencian del timeout?
El circuit breaker (literalmente, “disyuntor”, como el de tu tablero eléctrico) trackea los fallos a lo largo de muchas requests. Cuando los fallos cruzan un umbral, “abre” el circuito: rechaza las llamadas al instante, sin siquiera intentar la red. Después de un período de enfriamiento (cooldown), deja pasar una request de prueba para ver si el servicio se recuperó.
Tiene tres estados:
- CLOSED (cerrado): todo normal, las requests pasan. Es el estado de operación sana.
- OPEN (abierto): demasiados fallos seguidos. El breaker rechaza todo al instante y no toca la red. Acá está la diferencia con el timeout: no perdés ni 2 segundos por intento.
- HALF_OPEN (semiabierto): pasó el cooldown. Deja pasar una sola request de sondeo. Si funciona, vuelve a CLOSED. Si falla, vuelve a OPEN y arranca otro cooldown.
¿La distinción clave? El timeout protege la request individual. El breaker protege al sistema entero de seguir martillando algo que ya sabés que está roto.
¿Cómo implementar un circuit breaker en Node.js?
Una clase de unas 30 líneas alcanza. Lleva el conteo de fallos, el umbral, el cooldown y la lógica de los tres estados. Más contexto en aplicaciones que atienden múltiples regiones.
class CircuitBreaker {
constructor({ threshold = 5, cooldown = 10000 } = {}) {
this.threshold = threshold; // fallos para abrir
this.cooldown = cooldown; // ms de enfriamiento
this.failures = 0;
this.state = "CLOSED"; // CLOSED | OPEN | HALF_OPEN
this.nextTry = 0;
}
async call(fn) {
if (this.state === "OPEN") {
if (Date.now() < this.nextTry) {
throw new Error("Circuit OPEN, fallando rápido");
}
this.state = "HALF_OPEN"; // hora de tantear el terreno
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (err) {
this.onFailure();
throw err;
}
}
onSuccess() {
this.failures = 0;
this.state = "CLOSED";
}
onFailure() {
this.failures++;
if (this.failures >= this.threshold) {
this.state = "OPEN";
this.nextTry = Date.now() + this.cooldown;
}
}
}La lógica es directa. Cada fallo suma uno al contador. Al llegar al threshold, el estado pasa a OPEN y se agenda el próximo intento. Mientras esté OPEN y no haya pasado el cooldown, todas las llamadas mueren al instante. Cuando pasa el tiempo, el breaker se pone HALF_OPEN y deja entrar esa request de sondeo que decide si volvemos a la normalidad.
¿Cómo combinar circuit breakers y timeouts?
Acá está la gracia. El breaker envuelve la request con timeout. Así, una dependencia lenta que se vence por el timeout cuenta como fallo, suma al contador y eventualmente abre el circuito.
const breaker = new CircuitBreaker({ threshold: 5, cooldown: 10000 });
async function getUser(id) {
return breaker.call(() =>
fetchConTimeout(`https://api.example.com/users/${id}`, 2000)
);
}El flujo queda así: la request entra al breaker, el breaker decide si la deja pasar o no, y si pasa la ejecuta con su timeout de 2 segundos. Si la API responde rápido, bárbaro. Si se vuelve lenta, los timeouts se acumulan, el breaker llega al umbral y abre. A partir de ahí dejás de gastar 2 segundos por intento: fallás en microsegundos y, lo ideal, devolvés un fallback (un valor cacheado, una respuesta degradada, lo que tenga sentido para tu caso).
¿Cuándo usar cada patrón? Tabla y valores recomendados
No siempre necesitás los dos. La regla rápida: timeout siempre, breaker cuando una dependencia externa puede caerse y arrastrarte. En reducir dependencias de APIs externas profundizamos sobre esto.
| Aspecto | Timeout | Circuit Breaker |
|---|---|---|
| Qué protege | Una request individual | El sistema entero |
| Cómo decide | Tiempo de una llamada | Fallos acumulados en varias llamadas |
| Estados | Ninguno (binario) | CLOSED, OPEN, HALF_OPEN |
| Costo cuando la API está caída | El timeout completo por intento | Casi cero (rechazo instantáneo) |
| Cuándo usarlo | Siempre, en toda llamada de red | Dependencias externas que pueden fallar |
| Valor de arranque típico | 2 a 5 segundos | Umbral 5 fallos, cooldown 10 s |

Los valores de arranque (2 a 5 segundos de timeout, umbral de 5 fallos, cooldown de 10 segundos) son puntos de partida razonables, no verdades reveladas. Tomalos con pinzas y ajustalos midiendo tu propio tráfico y la latencia real de cada dependencia.
Errores comunes con circuit breakers y timeouts
- Confiar en el timeout por defecto del cliente HTTP: no hay default seguro, casi siempre es “esperar para siempre”. Si no pusiste un timeout explícito, asumí que no tenés ninguno.
- Timeout demasiado corto: si lo ponés en 200 ms cuando la API normal tarda 400 ms, vas a fallar requests sanas y disparar el breaker sin motivo. Medí la latencia real (p95, p99) antes de fijar el número.
- Umbral demasiado bajo: abrir el circuito al primer fallo te deja sin servicio por un pico transitorio que se hubiera resuelto solo. Dale margen.
- Breaker sin cooldown ni HALF_OPEN: si abrís y nunca probás de nuevo, te quedás caído aunque la dependencia ya se haya recuperado. El estado de sondeo es lo que te devuelve a la vida.
- No tener fallback cuando el circuito está OPEN: fallar rápido está bien, pero si podés devolver un valor cacheado o una respuesta degradada, el usuario ni se entera.
Preguntas Frecuentes
¿Cuál es la diferencia entre timeout y circuit breaker?
El timeout protege una request individual cortándola si tarda más de lo permitido. El circuit breaker protege al sistema entero: cuenta los fallos a lo largo de muchas requests y, al cruzar un umbral, deja de llamar a la dependencia caída para no seguir gastando recursos.
¿Por qué mi aplicación se cuelga cuando una API externa falla?
Porque la mayoría de los clientes HTTP esperan para siempre por defecto. Una dependencia lenta retiene conexiones que nunca se liberan, agota tu connection pool y bloquea hasta los requests que no dependen de esa API. La solución es poner un timeout explícito en cada llamada de red.
¿Qué timeout debería poner en mis requests HTTP?
Un punto de partida razonable es entre 2 y 5 segundos, pero el valor correcto depende de la latencia real de cada dependencia. Medí el percentil p95 o p99 de respuesta y poné el timeout un poco por encima, así no cortás requests sanas pero igual fallás rápido ante un estancamiento.
¿Necesito una librería para usar circuit breakers en Node.js?
No. Una clase de unas 30 líneas con un contador de fallos, un umbral y un cooldown alcanza para un breaker funcional, como mostramos arriba. Para producción a gran escala existen librerías maduras que agregan métricas y configuración fina, pero el patrón en sí no requiere ninguna dependencia.
¿Qué significa que un circuit breaker está en estado HALF_OPEN?
HALF_OPEN es el estado de sondeo. Después del cooldown, el breaker deja pasar una sola request de prueba. Si esa request funciona, vuelve a CLOSED y reanuda el tráfico normal. Si falla, vuelve a OPEN y arranca otro período de enfriamiento.
Conclusión
Llamar a otra API es heredar su peor día, y la única defensa que tenés es ponerle límites antes de que te arrastre. Empezá por lo más simple y de mayor impacto: un timeout explícito en cada llamada de red. Si no tenés ninguno hoy, ese es tu incidente de producción esperando a pasar.
Cuando tengas dependencias externas que pueden caerse, sumá el circuit breaker por encima del timeout. El breaker envuelve la request con timeout, así una API lenta cuenta como fallo y eventualmente abre el circuito para que dejes de golpear algo roto. Son dos patrones viejos, sin onda, y son lo que separa un servicio que aguanta un mal día de uno que se cae con la primera dependencia que falla.






