Front-end Engineering Lab

Circuit Breaker Pattern

Prevent cascading failures by detecting and isolating failing services

Circuit Breaker Pattern (Frontend)

The circuit breaker pattern prevents your app from repeatedly calling a failing service, reducing load and providing faster feedback to users.

How It Works

A circuit breaker has three states:

CLOSED (Normal)
    ↓ (failures exceed threshold)
OPEN (Blocking calls)
    ↓ (timeout expires)
HALF-OPEN (Testing)
    ↓ (success/failure)
CLOSED or OPEN

States Explained

CLOSED (Default):

  • Requests pass through normally
  • Failures are counted
  • If threshold exceeded → OPEN

OPEN (Failing):

  • Requests fail immediately (fail-fast)
  • No calls to failing service
  • After timeout → HALF-OPEN

HALF-OPEN (Testing):

  • Allow limited test requests
  • Success → CLOSED
  • Failure → OPEN

Basic Implementation

// lib/circuit-breaker.ts
export enum CircuitState {
  CLOSED = 'CLOSED',
  OPEN = 'OPEN',
  HALF_OPEN = 'HALF_OPEN',
}

interface CircuitBreakerConfig {
  failureThreshold: number;    // Open after N failures
  successThreshold: number;    // Close after N successes in HALF_OPEN
  timeout: number;             // Time to wait before HALF_OPEN (ms)
  halfOpenMaxCalls: number;   // Max concurrent calls in HALF_OPEN
}

export class CircuitBreaker {
  private state: CircuitState = CircuitState.CLOSED;
  private failureCount = 0;
  private successCount = 0;
  private nextAttempt: number = Date.now();
  private halfOpenCalls = 0;

  constructor(
    private name: string,
    private config: CircuitBreakerConfig = {
      failureThreshold: 5,
      successThreshold: 2,
      timeout: 60000, // 1 minute
      halfOpenMaxCalls: 3,
    }
  ) {}

  async execute<T>(fn: () => Promise<T>): Promise<T> {
    // Check if circuit should transition to HALF_OPEN
    if (this.state === CircuitState.OPEN) {
      if (Date.now() < this.nextAttempt) {
        throw new CircuitBreakerError(`Circuit breaker "${this.name}" is OPEN`);
      }
      
      this.state = CircuitState.HALF_OPEN;
      this.halfOpenCalls = 0;
      console.log(`[Circuit Breaker: ${this.name}] → HALF_OPEN`);
    }

    // Limit concurrent calls in HALF_OPEN
    if (this.state === CircuitState.HALF_OPEN) {
      if (this.halfOpenCalls >= this.config.halfOpenMaxCalls) {
        throw new CircuitBreakerError(`Circuit breaker "${this.name}" is testing (HALF_OPEN)`);
      }
      this.halfOpenCalls++;
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  private onSuccess() {
    this.failureCount = 0;

    if (this.state === CircuitState.HALF_OPEN) {
      this.successCount++;
      
      if (this.successCount >= this.config.successThreshold) {
        this.state = CircuitState.CLOSED;
        this.successCount = 0;
        console.log(`[Circuit Breaker: ${this.name}] → CLOSED`);
      }
    }
  }

  private onFailure() {
    this.failureCount++;
    this.successCount = 0;

    if (
      this.state === CircuitState.CLOSED &&
      this.failureCount >= this.config.failureThreshold
    ) {
      this.state = CircuitState.OPEN;
      this.nextAttempt = Date.now() + this.config.timeout;
      console.log(`[Circuit Breaker: ${this.name}] → OPEN`);
    }

    if (this.state === CircuitState.HALF_OPEN) {
      this.state = CircuitState.OPEN;
      this.nextAttempt = Date.now() + this.config.timeout;
      console.log(`[Circuit Breaker: ${this.name}] → OPEN (from HALF_OPEN)`);
    }
  }

  getState() {
    return {
      state: this.state,
      failureCount: this.failureCount,
      successCount: this.successCount,
      nextAttempt: new Date(this.nextAttempt),
    };
  }

  reset() {
    this.state = CircuitState.CLOSED;
    this.failureCount = 0;
    this.successCount = 0;
    this.halfOpenCalls = 0;
    console.log(`[Circuit Breaker: ${this.name}] → RESET`);
  }
}

export class CircuitBreakerError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'CircuitBreakerError';
  }
}

Usage with API Calls

// lib/api-client.ts
import { CircuitBreaker } from './circuit-breaker';

// Create circuit breakers for different services
const breakers = {
  recommendations: new CircuitBreaker('recommendations', {
    failureThreshold: 3,
    timeout: 30000, // 30s
  }),
  
  analytics: new CircuitBreaker('analytics', {
    failureThreshold: 5,
    timeout: 60000, // 1min
  }),
  
  payments: new CircuitBreaker('payments', {
    failureThreshold: 2, // More sensitive
    timeout: 120000, // 2min
  }),
};

export async function fetchRecommendations() {
  return breakers.recommendations.execute(async () => {
    const response = await fetch('/api/recommendations');
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    
    return response.json();
  });
}

// Usage in component
function Recommendations() {
  const { data, error } = useQuery('recommendations', fetchRecommendations);
  
  if (error instanceof CircuitBreakerError) {
    return (
      <div className="service-unavailable">
        <p>Recommendations are temporarily unavailable.</p>
        <p>We're working on it. Please try again later.</p>
      </div>
    );
  }
  
  if (error) {
    return <ErrorUI error={error} />;
  }
  
  return <RecommendationsList data={data} />;
}

React Hook Integration

// hooks/useCircuitBreaker.ts
import { useState, useEffect } from 'react';
import { CircuitBreaker, CircuitState } from '@/lib/circuit-breaker';

export function useCircuitBreaker<T>(
  name: string,
  fn: () => Promise<T>,
  options?: {
    enabled?: boolean;
    onStateChange?: (state: CircuitState) => void;
  }
) {
  const [breaker] = useState(() => new CircuitBreaker(name));
  const [state, setState] = useState(breaker.getState());

  // Monitor state changes
  useEffect(() => {
    const interval = setInterval(() => {
      const currentState = breaker.getState();
      setState(currentState);
      
      if (options?.onStateChange && currentState.state !== state.state) {
        options.onStateChange(currentState.state);
      }
    }, 1000);

    return () => clearInterval(interval);
  }, [breaker, options, state.state]);

  const execute = async () => {
    if (!options?.enabled) {
      return fn();
    }
    
    return breaker.execute(fn);
  };

  return {
    execute,
    state: state.state,
    stats: state,
    reset: () => breaker.reset(),
  };
}

// Usage
function DataComponent() {
  const { execute, state, reset } = useCircuitBreaker(
    'data-service',
    () => fetchData(),
    {
      enabled: true,
      onStateChange: (newState) => {
        if (newState === CircuitState.OPEN) {
          toast.error('Service temporarily unavailable');
        }
      },
    }
  );

  const { data, error } = useQuery('data', execute);

  return (
    <div>
      {state === CircuitState.OPEN && (
        <Banner type="error">
          Service is unavailable. Retrying automatically...
        </Banner>
      )}
      
      {state === CircuitState.HALF_OPEN && (
        <Banner type="warning">
          Testing service connection...
        </Banner>
      )}
      
      {/* ... render data ... */}
    </div>
  );
}

Multiple Service Management

// lib/circuit-breaker-manager.ts
import { CircuitBreaker, CircuitState } from './circuit-breaker';

class CircuitBreakerManager {
  private breakers = new Map<string, CircuitBreaker>();

  getOrCreate(name: string, config?: any): CircuitBreaker {
    if (!this.breakers.has(name)) {
      this.breakers.set(name, new CircuitBreaker(name, config));
    }
    return this.breakers.get(name)!;
  }

  getStatus() {
    const status: Record<string, any> = {};
    
    this.breakers.forEach((breaker, name) => {
      status[name] = breaker.getState();
    });
    
    return status;
  }

  getHealthScore(): number {
    const states = Array.from(this.breakers.values()).map(b => b.getState().state);
    
    if (states.length === 0) return 1;
    
    const closed = states.filter(s => s === CircuitState.CLOSED).length;
    return closed / states.length;
  }

  resetAll() {
    this.breakers.forEach(breaker => breaker.reset());
  }
}

export const circuitBreakerManager = new CircuitBreakerManager();

// Usage
const breaker = circuitBreakerManager.getOrCreate('recommendations');
await breaker.execute(() => fetchRecommendations());

Visual Indicators

// components/CircuitBreakerStatus.tsx
'use client';

import { useEffect, useState } from 'react';
import { circuitBreakerManager } from '@/lib/circuit-breaker-manager';
import { CircuitState } from '@/lib/circuit-breaker';

export function CircuitBreakerStatus() {
  const [status, setStatus] = useState(circuitBreakerManager.getStatus());
  const [health, setHealth] = useState(circuitBreakerManager.getHealthScore());

  useEffect(() => {
    const interval = setInterval(() => {
      setStatus(circuitBreakerManager.getStatus());
      setHealth(circuitBreakerManager.getHealthScore());
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  // Only show in dev or for admins
  if (process.env.NODE_ENV === 'production' && !isAdmin()) {
    return null;
  }

  return (
    <div className="circuit-breaker-status">
      <h3>Services Health: {(health * 100).toFixed(0)}%</h3>
      
      <div className="breakers-list">
        {Object.entries(status).map(([name, state]: [string, any]) => (
          <div key={name} className="breaker-item">
            <span className="breaker-name">{name}</span>
            <span className={`breaker-state state-${state.state.toLowerCase()}`}>
              {state.state}
            </span>
            
            {state.state === CircuitState.OPEN && (
              <span className="retry-time">
                Retry in {Math.round((state.nextAttempt.getTime() - Date.now()) / 1000)}s
              </span>
            )}
          </div>
        ))}
      </div>
    </div>
  );
}

Advanced Patterns

Fallback with Circuit Breaker

// Combine circuit breaker with fallback data
async function getDataWithFallback() {
  try {
    return await breaker.execute(() => fetchData());
  } catch (error) {
    if (error instanceof CircuitBreakerError) {
      // Circuit is open, use fallback immediately
      console.log('Using fallback data (circuit open)');
      return getFallbackData();
    }
    
    // Other errors, still try fallback
    console.log('Using fallback data (fetch failed)');
    return getFallbackData();
  }
}

Cascading Circuit Breakers

// Primary service with fallback service
async function getRecommendations() {
  try {
    // Try primary service
    return await primaryBreaker.execute(() => fetchFromPrimary());
  } catch (error) {
    // Primary failed, try fallback service
    try {
      return await fallbackBreaker.execute(() => fetchFromFallback());
    } catch (fallbackError) {
      // Both failed, use static recommendations
      return getStaticRecommendations();
    }
  }
}

Metrics Collection

// Track circuit breaker metrics
class CircuitBreakerWithMetrics extends CircuitBreaker {
  private metrics = {
    totalCalls: 0,
    successfulCalls: 0,
    failedCalls: 0,
    rejectedCalls: 0, // Rejected by circuit
    stateChanges: [] as Array<{ from: CircuitState; to: CircuitState; timestamp: Date }>,
  };

  async execute<T>(fn: () => Promise<T>): Promise<T> {
    this.metrics.totalCalls++;
    
    try {
      const result = await super.execute(fn);
      this.metrics.successfulCalls++;
      return result;
    } catch (error) {
      if (error instanceof CircuitBreakerError) {
        this.metrics.rejectedCalls++;
      } else {
        this.metrics.failedCalls++;
      }
      throw error;
    }
  }

  getMetrics() {
    return {
      ...this.metrics,
      successRate: this.metrics.successfulCalls / this.metrics.totalCalls,
      failureRate: this.metrics.failedCalls / this.metrics.totalCalls,
      rejectionRate: this.metrics.rejectedCalls / this.metrics.totalCalls,
    };
  }
}

Testing Circuit Breaker

// __tests__/circuit-breaker.test.ts
import { CircuitBreaker, CircuitState } from '../circuit-breaker';

describe('CircuitBreaker', () => {
  it('opens after threshold failures', async () => {
    const breaker = new CircuitBreaker('test', {
      failureThreshold: 3,
      timeout: 1000,
    });

    const failingFn = () => Promise.reject(new Error('Failed'));

    // Fail 3 times
    for (let i = 0; i < 3; i++) {
      await expect(breaker.execute(failingFn)).rejects.toThrow('Failed');
    }

    // Circuit should be open now
    expect(breaker.getState().state).toBe(CircuitState.OPEN);

    // Next call should be rejected immediately
    await expect(breaker.execute(failingFn)).rejects.toThrow('Circuit breaker');
  });

  it('transitions to half-open after timeout', async () => {
    const breaker = new CircuitBreaker('test', {
      failureThreshold: 2,
      timeout: 100, // 100ms
    });

    const failingFn = () => Promise.reject(new Error('Failed'));

    // Open the circuit
    await expect(breaker.execute(failingFn)).rejects.toThrow();
    await expect(breaker.execute(failingFn)).rejects.toThrow();

    expect(breaker.getState().state).toBe(CircuitState.OPEN);

    // Wait for timeout
    await new Promise(resolve => setTimeout(resolve, 150));

    // Next call should transition to HALF_OPEN
    const successFn = () => Promise.resolve('success');
    await breaker.execute(successFn);

    // Should still be HALF_OPEN (needs more successes)
    expect(breaker.getState().state).toBe(CircuitState.HALF_OPEN);
  });
});

When to Use Circuit Breaker

Use circuit breaker for:

  • External API calls
  • Third-party services
  • Microservices communication
  • Non-critical features (recommendations, analytics)
  • Services with known reliability issues

Don't use for:

  • Critical services (auth, payments)
  • Database queries (use connection pools instead)
  • Client-side operations
  • Services with SLA guarantees

Best Practices

  1. Different Thresholds: Critical services = stricter, optional = lenient
  2. Monitor State Changes: Alert when circuits open
  3. Provide Fallbacks: Always have Plan B
  4. Test Failure Scenarios: Simulate service outages
  5. Log State Transitions: Track when/why circuits open
  6. Progressive Timeout: Longer timeouts after repeated failures
  7. User Communication: Show when services are degraded

Common Mistakes

Same threshold for all services: Different criticality
Tune per service

No fallback: Users see errors
Always provide fallback

Too sensitive: Opens too easily
Balance sensitivity

No monitoring: Don't know when circuits open
Alert on state changes

Circuit Breaker Configuration Guide

Service TypeFailure ThresholdTimeoutHalf-Open Calls
Critical1-22-5 min1
Important3-51-2 min2-3
Optional5-1030-60s3-5
Analytics10+10-30s5-10

Circuit breakers are your first line of defense against cascading failures—use them to isolate problems before they spread.

On this page