The circuit breaker pattern: stop cascading failures before they happen
← Back
July 9, 2026Architecture7 min read

The circuit breaker pattern: stop cascading failures before they happen

Published July 9, 20267 min read

Our recommendation service went slow — 10-second timeouts instead of 200ms. Every API request that called it started taking 10 seconds. The connection pool filled up. Then the product page started timing out. Then the homepage. One slow microservice cascaded into a full outage. The circuit breaker pattern would have stopped this at the first service. Here is how it works.

How a circuit breaker works

CLOSED (normal) -> failures cross threshold -> OPEN (fast fail)
OPEN -> after timeout -> HALF-OPEN (test one request)
HALF-OPEN -> request succeeds -> CLOSED
HALF-OPEN -> request fails -> OPEN

Implementation in TypeScript

typescript
enum CircuitState {
  CLOSED = 'CLOSED',
  OPEN = 'OPEN',
  HALF_OPEN = 'HALF_OPEN',
}

interface CircuitBreakerOptions {
  failureThreshold: number;   // Failures before opening (default: 5)
  successThreshold: number;   // Successes to close from half-open (default: 2)
  openTimeoutMs: number;      // How long to stay open (default: 60000)
  callTimeoutMs: number;      // Max time for a call (default: 5000)
}

class CircuitBreaker {
  private state = CircuitState.CLOSED;
  private failureCount = 0;
  private successCount = 0;
  private lastFailureTime: number | null = null;

  constructor(
    private fn: (...args: unknown[]) => Promise,
    private options: CircuitBreakerOptions = {
      failureThreshold: 5,
      successThreshold: 2,
      openTimeoutMs: 60_000,
      callTimeoutMs: 5_000,
    }
  ) {}

  async call(...args: unknown[]): Promise {
    if (this.state === CircuitState.OPEN) {
      if (Date.now() - (this.lastFailureTime ?? 0) > this.options.openTimeoutMs) {
        this.state = CircuitState.HALF_OPEN;
        this.successCount = 0;
      } else {
        throw new CircuitOpenError('Circuit is OPEN — call rejected');
      }
    }

    try {
      const result = await Promise.race([
        this.fn(...args),
        this.timeout(),
      ]) as T;

      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  private timeout(): Promise {
    return new Promise((_, reject) =>
      setTimeout(() => reject(new Error('Call timed out')), this.options.callTimeoutMs)
    );
  }

  private onSuccess(): void {
    this.failureCount = 0;
    if (this.state === CircuitState.HALF_OPEN) {
      this.successCount++;
      if (this.successCount >= this.options.successThreshold) {
        this.state = CircuitState.CLOSED;
        console.log('Circuit CLOSED — service recovered');
      }
    }
  }

  private onFailure(): void {
    this.failureCount++;
    this.lastFailureTime = Date.now();
    
    if (
      this.state === CircuitState.HALF_OPEN ||
      this.failureCount >= this.options.failureThreshold
    ) {
      this.state = CircuitState.OPEN;
      console.log(`Circuit OPEN — ${this.failureCount} failures`);
    }
  }

  getState(): CircuitState {
    return this.state;
  }
}

class CircuitOpenError extends Error {}

Using the circuit breaker

typescript
async function fetchRecommendations(userId: string): Promise {
  const response = await fetch(`https://recommendations.internal/users/${userId}`);
  if (!response.ok) throw new Error('Recommendations service error');
  return response.json();
}

// Wrap with circuit breaker
const recommendationsBreaker = new CircuitBreaker(fetchRecommendations, {
  failureThreshold: 3,   // Open after 3 consecutive failures
  openTimeoutMs: 30_000, // Try again after 30 seconds
  callTimeoutMs: 2_000,  // Fail fast after 2 seconds
  successThreshold: 2,
});

// In your route handler
async function getProductPage(req: Request, res: Response) {
  const product = await getProduct(req.params.id);
  
  let recommendations: Product[] = [];
  try {
    recommendations = await recommendationsBreaker.call(req.userId);
  } catch (error) {
    if (error instanceof CircuitOpenError) {
      // Circuit is open — fail gracefully with empty recommendations
      console.log('Recommendations circuit open — using empty fallback');
    } else {
      // Actual call failed — this failure was counted
      console.error('Recommendations service error:', error);
    }
    // Page renders without recommendations instead of timing out
  }
  
  res.json({ product, recommendations });
}

Library alternatives

bash
# Don't build from scratch in production — use a battle-tested library
npm install opossum        # Node.js circuit breaker (most popular)
pip install pybreaker      # Python circuit breaker
typescript
import CircuitBreaker from 'opossum';

const breaker = new CircuitBreaker(fetchRecommendations, {
  timeout: 2000,
  errorThresholdPercentage: 50, // Open if 50% of calls fail
  resetTimeout: 30000,
});

breaker.on('open', () => console.log('Circuit opened'));
breaker.on('halfOpen', () => console.log('Circuit half-opened'));
breaker.on('close', () => console.log('Circuit closed'));

// Built-in fallback support
breaker.fallback(() => []);  // Return empty array when open

const recommendations = await breaker.fire(userId);

The circuit breaker pattern is essential for any distributed system. Without it, slow downstream services cause thread/connection exhaustion upstream, which cascades. With it, failures are contained at the boundary of the failing service and the rest of the system degrades gracefully rather than collapsing.

Share this
← All Posts7 min read