The circuit breaker middleware prevents cascading failures by automatically “opening the circuit” when a task fails repeatedly. Once open, subsequent calls fail immediately without attempting the operation, giving the failing service time to recover.
How It Works
A circuit breaker has three states:
CLOSED (Normal Operation)
Requests pass through normally. Failures are counted.
OPEN (Failing Fast)
After reaching the failure threshold, the circuit opens. All requests fail immediately with CircuitBreakerOpenError without attempting the operation.
HALF_OPEN (Testing Recovery)
After the reset timeout, the circuit enters HALF_OPEN state. The next request is allowed through as a “probe”. If it succeeds, the circuit closes. If it fails, the circuit reopens.
When to Use Circuit Breaker
External services Protect against unreliable third-party APIs
Database connections Fail fast when database is overloaded
Microservices Prevent cascade failures in distributed systems
Rate-limited APIs Avoid hammering APIs that are rejecting requests
Quick Start
import { r , globals } from "@bluelibs/runner" ;
const callExternalAPI = r
. task ( "api.external" )
. middleware ([
globals . middleware . task . circuitBreaker . with ({
failureThreshold: 5 , // Open after 5 failures
resetTimeout: 30000 , // Try again after 30 seconds
})
])
. run ( async ( url : string ) => {
const response = await fetch ( url );
if ( ! response . ok ) throw new Error ( `HTTP ${ response . status } ` );
return response . json ();
})
. build ();
After 5 consecutive failures, the circuit opens. For the next 30 seconds, all calls fail immediately. After 30 seconds, one probe request is allowed through.
Configuration
Number of consecutive failures before the circuit opens. Example: 5 means the circuit opens after the 5th failure.
Time in milliseconds to wait before transitioning from OPEN to HALF_OPEN. Example: 30000 means wait 30 seconds before allowing a probe request.
Examples
Basic Circuit Breaker
import { r , globals } from "@bluelibs/runner" ;
const fetchUser = r
. task ( "users.fetch" )
. middleware ([
globals . middleware . task . circuitBreaker . with ({
failureThreshold: 3 ,
resetTimeout: 60000 , // 1 minute
})
])
. run ( async ( userId : string ) => {
return database . users . findOne ({ id: userId });
})
. build ();
Handling Circuit Breaker Errors
import { r , globals , run } from "@bluelibs/runner" ;
import { CircuitBreakerOpenError } from "@bluelibs/runner/globals/middleware/circuitBreaker.middleware" ;
const apiCall = r
. task ( "api.call" )
. middleware ([
globals . middleware . task . circuitBreaker . with ({
failureThreshold: 5 ,
resetTimeout: 30000 ,
})
])
. run ( async ( url : string ) => fetch ( url ). then ( r => r . json ()))
. build ();
const app = r . resource ( "app" ). register ([ apiCall ]). build ();
const { runTask , dispose } = await run ( app );
try {
const result = await runTask ( apiCall , "https://api.example.com/data" );
} catch ( error ) {
if ( error instanceof CircuitBreakerOpenError ) {
console . error ( "Circuit is open:" , error . message );
// "Circuit is OPEN for task 'api.call'"
// Return fallback data or queue for later
return fallbackData ;
}
throw error ;
}
await dispose ();
Different Thresholds for Different Operations
// Aggressive circuit breaker for critical path
const criticalAPI = r
. task ( "api.critical" )
. middleware ([
globals . middleware . task . circuitBreaker . with ({
failureThreshold: 2 , // Open after just 2 failures
resetTimeout: 10000 , // Retry after 10 seconds
})
])
. run ( async ( input ) => criticalOperation ( input ))
. build ();
// Lenient circuit breaker for background jobs
const backgroundJob = r
. task ( "jobs.background" )
. middleware ([
globals . middleware . task . circuitBreaker . with ({
failureThreshold: 10 , // Tolerate more failures
resetTimeout: 120000 , // Wait 2 minutes before retry
})
])
. run ( async ( input ) => processJob ( input ))
. build ();
Combining with Other Middleware
Circuit Breaker + Retry
import { r , globals } from "@bluelibs/runner" ;
const resilientAPI = r
. task ( "api.resilient" )
. middleware ([
globals . middleware . task . retry . with ({ retries: 2 }),
globals . middleware . task . circuitBreaker . with ({
failureThreshold: 5 ,
resetTimeout: 30000 ,
}),
])
. run ( async ( url : string ) => fetch ( url ). then ( r => r . json ()))
. build ();
Order matters! Retry runs before circuit breaker, so each retry attempt counts as a separate request. If the circuit is OPEN, retries won’t help—the circuit breaker will reject immediately.
Circuit Breaker + Timeout
const protectedAPI = r
. task ( "api.protected" )
. middleware ([
globals . middleware . task . timeout . with ({ ttl: 5000 }),
globals . middleware . task . circuitBreaker . with ({
failureThreshold: 5 ,
resetTimeout: 30000 ,
}),
])
. run ( async ( url : string ) => fetch ( url ). then ( r => r . json ()))
. build ();
Timeout errors count as failures and contribute to opening the circuit.
Circuit Breaker + Fallback
const withFallback = r
. task ( "api.withFallback" )
. middleware ([
globals . middleware . task . circuitBreaker . with ({
failureThreshold: 3 ,
resetTimeout: 60000 ,
}),
])
. run ( async ( url : string ) => {
try {
return await fetch ( url ). then ( r => r . json ());
} catch ( error ) {
if ( error instanceof CircuitBreakerOpenError ) {
// Circuit is open, return cached data
return await getCachedData ( url );
}
throw error ;
}
})
. build ();
Execution Journal
The circuit breaker middleware exposes state via the execution journal:
import { r , globals } from "@bluelibs/runner" ;
import { journalKeys , CircuitBreakerState } from "@bluelibs/runner/globals/middleware/circuitBreaker.middleware" ;
const monitoredTask = r
. task ( "api.monitored" )
. middleware ([
globals . middleware . task . circuitBreaker . with ({
failureThreshold: 5 ,
resetTimeout: 30000 ,
})
])
. run ( async ( url : string , deps , { journal }) => {
// Check circuit state
const state = journal . get ( journalKeys . state );
const failures = journal . get ( journalKeys . failures );
console . log ( `Circuit state: ${ state } ` );
console . log ( `Failure count: ${ failures } ` );
if ( state === CircuitBreakerState . HALF_OPEN ) {
console . log ( "Probe request - circuit testing recovery" );
}
return fetch ( url ). then ( r => r . json ());
})
. build ();
Journal Keys
Current state: CLOSED, OPEN, or HALF_OPEN
Current consecutive failure count
Circuit Breaker States
The CircuitBreakerState enum has three values:
import { CircuitBreakerState } from "@bluelibs/runner/globals/middleware/circuitBreaker.middleware" ;
CircuitBreakerState . CLOSED // Normal operation
CircuitBreakerState . OPEN // Failing fast
CircuitBreakerState . HALF_OPEN // Testing recovery
Common Patterns
Circuit Breaker with Metrics
import { r , globals } from "@bluelibs/runner" ;
import { journalKeys , CircuitBreakerState } from "@bluelibs/runner/globals/middleware/circuitBreaker.middleware" ;
const monitoredAPI = r
. task ( "api.monitored" )
. dependencies ({ logger: globals . resources . logger })
. middleware ([
globals . middleware . task . circuitBreaker . with ({
failureThreshold: 5 ,
resetTimeout: 30000 ,
})
])
. run ( async ( url : string , { logger }, { journal }) => {
const state = journal . get ( journalKeys . state );
const failures = journal . get ( journalKeys . failures );
// Log circuit state changes
if ( state === CircuitBreakerState . OPEN ) {
await logger . error ( "Circuit breaker opened" , {
data: { failures , task: "api.monitored" }
});
} else if ( state === CircuitBreakerState . HALF_OPEN ) {
await logger . info ( "Circuit breaker testing recovery" );
}
// Send metrics
metrics . gauge ( 'circuit_breaker.failures' , failures , { task: 'api.monitored' });
metrics . gauge ( 'circuit_breaker.state' , state === CircuitBreakerState . CLOSED ? 0 : 1 );
return fetch ( url ). then ( r => r . json ());
})
. build ();
Per-User Circuit Breaker
The default circuit breaker is per-task. For per-user or per-tenant circuit breaking, use custom middleware:
import { r } from "@bluelibs/runner" ;
// Custom circuit breaker with per-user state
const perUserCircuitBreaker = r . middleware
. task ( "app.middleware.perUserCircuitBreaker" )
. run ( async ({ task , next }, deps , config : { failureThreshold : number }) => {
const userId = task . input . userId ;
const state = getUserCircuitState ( userId ); // Your state management
if ( state . failures >= config . failureThreshold ) {
if ( Date . now () - state . lastFailure < 30000 ) {
throw new Error ( `Circuit open for user ${ userId } ` );
}
}
try {
const result = await next ( task . input );
// Reset on success
state . failures = 0 ;
return result ;
} catch ( error ) {
state . failures ++ ;
state . lastFailure = Date . now ();
throw error ;
}
})
. build ();
Circuit Breaker with Alert
const alertingAPI = r
. task ( "api.alerting" )
. dependencies ({ alertService: alertServiceResource })
. middleware ([
globals . middleware . task . circuitBreaker . with ({
failureThreshold: 5 ,
resetTimeout: 30000 ,
})
])
. run ( async ( url : string , { alertService }, { journal }) => {
const state = journal . get ( journalKeys . state );
const failures = journal . get ( journalKeys . failures );
// Alert on circuit open
if ( state === CircuitBreakerState . OPEN && failures === 5 ) {
await alertService . send ({
severity: 'critical' ,
message: `Circuit breaker opened for api.alerting after ${ failures } failures` ,
});
}
return fetch ( url ). then ( r => r . json ());
})
. build ();
Internal State Management
The circuit breaker middleware uses a shared resource to maintain state:
import { circuitBreakerResource } from "@bluelibs/runner/globals/middleware/circuitBreaker.middleware" ;
// The resource is automatically registered when you use the middleware
// State is per-task-ID, stored in a Map
Circuit state is per task definition , not per task instance. All invocations of the same task share the same circuit breaker state.
Best Practices
Set appropriate thresholds for your use case
Critical services should fail fast; background jobs can be more tolerant: // User-facing: fail fast
failureThreshold : 3 ,
resetTimeout : 10000 ,
// Background job: more tolerant
failureThreshold : 10 ,
resetTimeout : 120000 ,
Combine with retry for transient failures
Use retry for transient issues, circuit breaker for systemic failures: . middleware ([
globals . middleware . task . retry . with ({ retries: 2 }),
globals . middleware . task . circuitBreaker . with ({ failureThreshold: 5 }),
])
Monitor circuit state in production
Track when circuits open to identify failing services: if ( state === CircuitBreakerState . OPEN ) {
metrics . increment ( 'circuit_breaker.open' , { task: taskId });
logger . error ( 'Circuit breaker opened' , { task: taskId , failures });
}
Provide fallback values when circuit is open
Don’t just fail—return cached or default data: try {
return await fetch ( url );
} catch ( error ) {
if ( error instanceof CircuitBreakerOpenError ) {
return cachedData ?? defaultData ;
}
throw error ;
}
Use longer reset timeouts for external services
Give external services time to recover: // Internal service: quick recovery
resetTimeout : 10000 , // 10 seconds
// External API: longer recovery time
resetTimeout : 120000 , // 2 minutes
Error Details
The CircuitBreakerOpenError extends RunnerError:
import { CircuitBreakerOpenError } from "@bluelibs/runner/globals/middleware/circuitBreaker.middleware" ;
try {
await runTask ( myTask , input );
} catch ( error ) {
if ( error instanceof CircuitBreakerOpenError ) {
console . log ( error . message ); // "Circuit is OPEN for task 'api.call'"
console . log ( error . id ); // "runner.errors.middlewareCircuitBreakerOpen"
console . log ( error . httpCode ); // 503 (Service Unavailable)
}
}
See Also
Retry Middleware Handle transient failures with retries
Timeout Middleware Prevent hanging operations
Rate Limit Middleware Control request frequency