| name | resilience-circuit-breakers |
| description | Resilience patterns for AI agent external calls. Circuit breakers, exponential backoff with jitter, stream recovery, batch error handling, and log fallback under disk pressure. Sources: nodeshift/opossum, softonic/axios-retry, vimeo/player.js, trentm/node-bunyan, visionmedia/batch. |
/resilience-circuit-breakers
When to Use
- Agent calls external LLM API, WebFetch, or DB that can fail/rate-limit
- Stream-based data pipeline where disconnects must not lose data
- Batch of agent tasks where some fail and need retry without blocking others
- "Agent crashes on every 429 / timeout instead of backing off"
Do NOT use for
- Internal in-process function calls (no network = no circuit needed)
- One-shot scripts that don't retry
Circuit Breaker (opossum)
import CircuitBreaker from 'opossum'
const callLLM = async (prompt) => fetch('/api/llm', { method: 'POST', body: prompt })
const breaker = new CircuitBreaker(callLLM, {
timeout: 5000,
errorThresholdPct: 50,
resetTimeout: 30000,
volumeThreshold: 5,
})
breaker.fallback(() => ({ cached: true, text: 'LLM unavailable — using cached response' }))
breaker.on('open', () => console.warn('[circuit] OPEN — LLM unreachable'))
breaker.on('halfOpen', () => console.log('[circuit] HALF-OPEN — testing'))
breaker.on('close', () => console.log('[circuit] CLOSED — restored'))
const result = await breaker.fire(prompt)
Exponential Backoff + Jitter (axios-retry)
import axios from 'axios'
import axiosRetry from 'axios-retry'
axiosRetry(axios, {
retries: 4,
retryDelay: (retryCount) => {
const base = axiosRetry.exponentialDelay(retryCount)
const jitter = Math.random() * 1000
return base + jitter
},
retryCondition: (error) =>
axiosRetry.isNetworkOrIdempotentRequestError(error) ||
error.response?.status === 429 ||
error.response?.status >= 500,
onRetry: (count, error) => {
console.warn(`[retry] attempt ${count} after: ${error.message}`)
},
})
Stream Recovery (Vimeo player.js pattern)
class ResumableStream {
#offset = 0
#buffer: Buffer[] = []
pipe(source: NodeJS.ReadableStream, sink: NodeJS.WritableStream) {
source.on('data', (chunk) => {
this.#buffer.push(chunk)
this.#offset += chunk.length
sink.write(chunk)
})
source.on('error', (err) => {
console.error(`[stream] error at offset ${this.#offset}:`, err.message)
this.#reconnect(sink)
})
}
#reconnect(sink: NodeJS.WritableStream) {
for (const chunk of this.#buffer) sink.write(chunk)
this.#buffer = []
}
}
Log Fallback Under Disk Pressure (bunyan pattern)
import bunyan from 'bunyan'
import fs from 'fs'
const log = bunyan.createLogger({
name: 'yamtam-agent',
streams: [
{
type: 'rotating-file',
path: 'releases/logs/agent.log',
period: '1d',
count: 7,
},
{
level: 'warn',
stream: process.stderr,
}
],
})
process.on('uncaughtException', (err) => {
if (err.code === 'ENOSPC') {
console.error('[log] disk full — switching to stderr only')
}
})
Batch Error Handling (visionmedia/batch)
import Batch from 'batch'
const batch = new Batch()
batch.concurrency(5)
agentSubTasks.forEach(task => {
batch.push((done) => {
executeAgentTask(task)
.then(result => done(null, result))
.catch(err => done(null, { error: err.message, task }))
})
})
batch.end((err, results) => {
if (err) return console.error('Batch aborted:', err)
const failed = results.filter(r => r.error)
const success = results.filter(r => !r.error)
console.()
failed.( retryQueue.( (f.)))
})
Anti-Fake-Pass Checklist
❌ Circuit breaker absent on LLM API calls (429 storm = agent crash loop)
❌ Retry without jitter on shared API (thundering herd)
❌ Retry on 4xx errors (400 = bad request, retrying won't fix it)
❌ Stream data dropped on error (must buffer and replay)
❌ Log write fails silently when disk full (must fall back to stderr)
❌ Batch aborts on first error (use done(null, err) to collect all results)
❌ resetTimeout missing on circuit breaker (open state = permanent outage)