Circuit breaker y kill switch en agentes: cortar la dependencia, no el proceso
Resumen
Un circuit breaker corta UNA dependencia (modelo, Postgres, MCP) cuando el fallo deja de ser transitorio: Closed, Open y Half-Open. Distinto de reintentos (por llamada) y del kill switch (apagar el agente entero). Azure Architecture + opossum 10.0.0. Cero retry contra Open. Fail-fast, no setTimeout decorativo.

Qué resuelve
Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.
Un agente no se cae porque el modelo “se equivoque de tono”. Se cae porque una llamada remota se queda colgada, el webhook ya contestó 200 y el proceso reintenta contra un 503 persistente. El Retry pattern asume fallo transitorio. El Circuit Breaker (Azure Architecture Center, 2026-09-06) asume lo contrario: temporarily block access to a remote service after failures reach a threshold, instead of repeatedly retrying an operation that's likely to fail.
No es reintentos. Eso es por llamada. Tampoco es el kill switch: eso apaga el agente entero (deleteWebhook, AGENT_DISABLED, fly scale 0). El breaker es por dependencia. El kill switch es por sistema.
Contrato: un breaker por dependencia crítica. Closed deja pasar. Open falla inmediato. Half-Open prueba con poquitas. Cero retry contra Open. Cero breaker global que apague el bot. El kill switch lo mueve un humano, no el umbral.
La regla de oro: tres estados, una dependencia
Azure describe el proxy como máquina de estados que imita un interruptor eléctrico:
| Estado | Qué hace | Cuándo sale |
|---|---|---|
| Closed | Enruta la llamada. Cuenta fallos recientes en una ventana de tiempo | Umbral de fallos → Open |
| Open | Falla inmediato. No llama al proveedor. Arranca un timer | Timer expira → Half-Open |
| Half-Open | Deja pasar un número limitado de pruebas | Éxitos consecutivos → Closed. Un fallo → Open otra vez |
El contador de Closed es temporal: se resetea solo. Un 429 aislado no abre. El umbral dispara Open solo si N fallos ocurren en un intervalo. Half-Open existe para no inundar un servicio que está recuperándose.
Fowler (2014): envuelves la llamada, cuentas fallos, y al cruzar el umbral las siguientes devuelven error sin tocar al proveedor. Azure: Retry y Circuit Breaker se combinan, pero the retry logic should be sensitive to any exceptions that the circuit breaker returns and stop retry attempts if the circuit breaker indicates that a fault isn't transient.

Qué cuenta como fallo (y qué no)
No todos los errores tripan el breaker. Fowler: Not all errors should trip the circuit, some should reflect normal failures. Azure pide mirar el tipo de excepción y ajustar el umbral. En un agente:
| Señal | ¿Cuenta para el umbral? | Por qué |
|---|---|---|
| Timeout / AbortSignal | Sí | El proveedor no contestó a tiempo |
| 429 / 500 / 503 / 529 | Sí | Transitorio hasta que deja de serlo |
| Conexión rehusada / DNS | Sí | La dependencia no está |
| 400 schema / 401 / 403 | No | Arreglas el request o rotas la clave; no esperas |
| 404 de un id inventado | No | Es lógica de negocio |
| Fallback del propio breaker | En opossum, sí | El README 10.0.0: When a fallback function is triggered, it's considered a failure |
El timeout de la llamada vive en AbortSignal, no en un setTimeout decorativo. El breaker usa ese timeout como fallo. No lo sustituye.
opossum 10.0.0: el contrato en Node
opossum (Apache-2.0, Node >= 22, latest 10.0.0 el 2026-09-06, docs en nodeshift.dev/opossum) ejecuta una función async y plays dead and fails fast. Defaults útiles para un tool de agente:
import CircuitBreaker from "opossum";
const callModel = (payload, signal) =>
fetch("https://api.openai.com/v1/responses", {
method: "POST",
body: JSON.stringify(payload),
signal,
}).then((r) => {
if (!r.ok) throw Object.assign(new Error(String(r.status)), { status: r.status });
return r.json();
});
const breaker = new CircuitBreaker(callModel, {
timeout: 8000,
errorThresholdPercentage: 50,
resetTimeout: 30_000,
volumeThreshold: 10,
errorFilter: (err) => err.status === 400 || err.status === 401 || err.status === 403,
});
breaker.fallback(() => ({ degraded: true, reason: "model_open" }));
breaker.on("open", () => metrics.inc("breaker_open", { dep: "openai" }));
breaker.on("halfOpen", () => metrics.inc("breaker_half_open", { dep: "openai" }));
breaker.on("close", () => metrics.inc("breaker_close", { dep: "openai" }));
timeout: 3000 en el README es el ejemplo; 8 s es más realista para un Responses call. errorThresholdPercentage: 50 es el 50 % de Fowler. resetTimeout: 30000 es Open → Half-Open. volumeThreshold: 10 evita abrir el circuito con tres llamadas de calentamiento (default 0). errorFilter truthy no incrementa fallos: 400/401/403 no son “el modelo está caído”.
Fallback no es éxito. opossum lo cuenta como fallo y lo sigue ejecutando hasta Closed. Devuelve JSON degradado (degraded: true), nunca un tool result que parezca éxito.
Serverless: breaker.toJSON() serializa state y status. Sin KV/Redis cada cold start arranca Closed. Un breaker en memoria de un Worker Free no es kill switch.
Un breaker por dependencia, no uno para el bot
Azure pide resource differentiation: no mezcles shards ni proveedores independientes en un solo umbral. En un agente:
- Modelo (OpenAI / Anthropic / Gemini): 50 % errores,
resetTimeout30–60 s. - Base de datos / cola: umbral más bajo (3–5 timeouts). Un Postgres saturado no se recupera a palos de retry.
- MCP / tool HTTP: breaker propio. Un servidor MCP colgado no debe abrir el del modelo.
- Canal (Telegram
getUpdates/ Slack): casi nunca. Si el canal muere, eso es kill switch, no Half-Open.
Half-Open limita las pruebas. 40 fire() en paralelo al reabrir tumban al proveedor. Una prueba basta.
El override de Azure es el puente al kill switch: an administrator can force a circuit breaker into the Open state. En prod: BREAKER_FORCE_OPEN=openai, distinto de AGENT_DISABLED.
Kill switch: otra palanca, otro dueño
| Palanca | Quién la mueve | Qué corta | Qué no hace |
|---|---|---|---|
| Retry + jitter | El runtime, por llamada | Nada; espera y reintenta | No protege si el fallo es persistente |
| Circuit breaker | El umbral, por dependencia | Llamadas a esa API | No borra el webhook ni las réplicas |
| Kill switch | Un humano (o runbook) | Canal + flag + réplicas | No diagnostica qué dependencia falló |
Si el agente alucina en un grupo, no esperes al breaker: deleteWebhook + drop_pending_updates. Si OpenAI lleva 20 min en 503, no apagues Telegram: abre el breaker del modelo, sirve fallback y alerta.
Observabilidad: evento en cada cambio de estado hacia logs. El healthcheck de liveness no falla porque una tool esté Open: el proceso vive; la dependencia no.

Checklist para el PR del agente
- Un
CircuitBreakernombrado por dependencia (openai,postgres,mcp_github). Cero singleton global. errorFilterpara 400/401/403. El schema roto no abre el circuito.- Timeout de la llamada = AbortSignal. El
timeoutde opossum aborta; no dejes elfetchhuérfano. - Fallback devuelve degradado explícito. Nunca un tool result que parezca éxito.
- Listeners
open/halfOpen/closecon label de dependencia. Alerta en Open, no en cada 429. - Half-Open: una prueba, no un
Promise.allde tools. - Flag de override
FORCE_OPEN/FORCE_CLOSEDpor dependencia, gated. Distinto deAGENT_DISABLED. - Ensayo: tumba el mock del modelo, verifica fail-fast < 50 ms, verifica que Telegram sigue contestando “degradado”.
- En serverless, documenta que el estado no viaja. Si importa, KV. Si no, cold start = Closed.
- Cero retry después de
CircuitBreakerOpen. Azure: el retry se detiene cuando el breaker dice que el fallo no es transitorio.
FAQ
¿Puedo usar el mismo breaker para el modelo y para Postgres? No. Umbrales distintos, recuperaciones distintas. Un 50 % de 429 de OpenAI no debe cortar los checkpoints.
¿El fallback cierra el circuito? No. En opossum 10.0.0 el fallback cuenta como fallo. Closed solo vuelve con éxitos reales en Half-Open.
¿Open es lo mismo que kill switch? No. Open corta una dependencia. El kill switch corta el canal y las réplicas. Un humano lo arma; un umbral no desinstala el bot.
¿Reintento dentro del breaker? Sí, antes de contar el fallo, y solo códigos transitorios. Si el breaker ya está Open, cero retry. Combina los dos patterns; no los fusiones en un while (true).
¿Dónde encaja el curso? En instalar un agente el runtime sale hablando. Esta guía es lo que pones antes de que un 503 convierta el webhook en un fork bomb de retries. Hub: seguridad, coste y operación.
Fuentes 2026-09-06: Azure Circuit Breaker + Retry, Fowler 2014, npm [email protected], nodeshift.dev/opossum.
Lecturas relacionadas
Sigue explorando AgentOps y otras piezas para builders.

LLM-as-judge para agentes: rúbrica, schema y calibración (el juez no es la verdad)

Versionar tool schemas de agentes: cambios solo aditivos y rollout sin romper producción

Ejecución durable para agentes largos: checkpoint por paso, resume sin repetir
