Skip to main content
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:
1

CLOSED (Normal Operation)

Requests pass through normally. Failures are counted.
2

OPEN (Failing Fast)

After reaching the failure threshold, the circuit opens. All requests fail immediately with CircuitBreakerOpenError without attempting the operation.
3

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

failureThreshold
number
default:"5"
Number of consecutive failures before the circuit opens.Example: 5 means the circuit opens after the 5th failure.
resetTimeout
number
default:"30000"
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

journalKeys.state
CircuitBreakerState
Current state: CLOSED, OPEN, or HALF_OPEN
journalKeys.failures
number
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

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,
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 }),
])
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 });
}
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;
}
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

Build docs developers (and LLMs) love