No description
Find a file
autocommit 277d05d410
Some checks failed
Build and Publish / build-and-publish (push) Failing after 39s
deps-upgrade(config): ⬆️ Update npm registry URL to .lan for local development consistency
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-10 04:20:03 -07:00
.forgejo/workflows fix(ci): fix backslash-bang syntax error in workflow 2026-01-30 15:49:19 -08:00
src chore: initial commit with publish config 2026-01-21 12:30:19 -08:00
.gitignore chore: initial commit with publish config 2026-01-21 12:30:19 -08:00
IMPLEMENTATION.md chore: initial commit with publish config 2026-01-21 12:30:19 -08:00
package.json deps-upgrade(deps-specific): ⬆️ Update dependencies to latest stable versions for bug fixes and performance improvements 2026-06-10 04:20:03 -07:00
README.md deps-upgrade(config): ⬆️ Update npm registry URL to .lan for local development consistency 2026-06-10 04:20:03 -07:00
tsconfig.json chore: initial commit with publish config 2026-01-21 12:30:19 -08:00
tsup.config.ts chore(build): 🔧 Update bundler config (tsup.config.ts) to refine minification/output targets 2026-01-21 15:26:43 -08:00

@lilith/circuit-breaker

Circuit breaker pattern implementation for resilient service calls.

Features

  • State Machine: Automatic transitions between CLOSED → OPEN → HALF-OPEN → CLOSED
  • Configurable Thresholds: Failure threshold, success threshold, timeout, volume threshold
  • Statistics Tracking: Real-time monitoring of failures, successes, consecutive counts, and state transitions
  • Thread-Safe: Atomic state transitions with execution locking for concurrent operations
  • Event Callbacks: React to state changes and failures
  • Type-Safe: Full TypeScript support with generic types
  • Zero Dependencies: No external runtime dependencies

Installation

pnpm add @lilith/circuit-breaker

Usage

Basic Example

import { CircuitBreaker } from '@lilith/circuit-breaker';

const breaker = new CircuitBreaker({
  failureThreshold: 5,      // Open after 5 consecutive failures
  successThreshold: 3,      // Close after 3 consecutive successes
  timeout: 30000,           // Wait 30s before trying half-open
  volumeThreshold: 10,      // Need 10 requests before evaluating
});

// Execute protected function
try {
  const user = await breaker.execute(async () => {
    return await userService.fetchUser(id);
  });
} catch (error) {
  if (error instanceof CircuitBreakerOpenError) {
    // Circuit is open, use fallback or cached data
    return getCachedUser(id);
  }
  throw error;
}

Monitoring State Changes

breaker.onStateChange((from, to) => {
  console.log(`Circuit breaker: ${from} -> ${to}`);
  if (to === 'open') {
    alerting.notify('Circuit breaker opened for user service');
  }
});

breaker.onFailure((error) => {
  console.error('Request failed:', error);
});

Accessing Statistics

const stats = breaker.statistics;
console.log({
  state: stats.state,
  failures: stats.failures,
  successes: stats.successes,
  totalRequests: stats.totalRequests,
  consecutiveFailures: stats.consecutiveFailures,
  consecutiveSuccesses: stats.consecutiveSuccesses,
  lastFailure: stats.lastFailure,
  lastSuccess: stats.lastSuccess,
  lastStateChange: stats.lastStateChange,
});

Manual Control

// Force open (useful for maintenance)
breaker.open();

// Force close (after manual intervention)
breaker.close();

// Transition to half-open (test recovery)
breaker.halfOpen();

Circuit States

CLOSED (Normal Operation)

  • Requests pass through normally
  • Failures and successes are tracked
  • Consecutive failures increment on each failure, reset on success
  • Opens when consecutiveFailures >= failureThreshold AND totalRequests >= volumeThreshold

OPEN (Failing Fast)

  • All requests immediately rejected with CircuitBreakerOpenError
  • No requests reach the protected service
  • After timeout milliseconds, transitions to HALF-OPEN

HALF-OPEN (Testing Recovery)

  • Requests allowed through to test recovery
  • Consecutive successes increment on each success
  • Any failure immediately reopens circuit (transitions back to OPEN)
  • After consecutiveSuccesses >= successThreshold, transitions to CLOSED

Configuration

interface CircuitBreakerOptions {
  failureThreshold: number;   // Default: 5
  successThreshold: number;   // Default: 3
  timeout: number;            // Default: 30000ms
  volumeThreshold: number;    // Default: 10
}

Configuration Guidelines

  • failureThreshold: Set based on expected error rate (higher for noisy services)
  • successThreshold: Lower values allow faster recovery (2-3 recommended)
  • timeout: Based on expected recovery time (30s default, increase for slow recovery)
  • volumeThreshold: Prevents opening on low traffic (10-20 recommended)

TypeScript Support

Full generic type support:

interface User {
  id: string;
  name: string;
}

const userBreaker = new CircuitBreaker<User>({
  failureThreshold: 3,
});

const user: User = await userBreaker.execute(async () => {
  return await api.getUser('123');
});

Thread Safety

The circuit breaker uses an execution lock to ensure atomic state transitions when multiple concurrent requests are made:

// Multiple concurrent requests are safe
const promises = Array.from({ length: 10 }, () =>
  breaker.execute(async () => {
    return await externalService.getData();
  })
);

const results = await Promise.all(promises);

State transitions are serialized to prevent race conditions:

  • Only one request can transition the state at a time
  • Consecutive failure/success counts are accurate
  • Volume threshold checks are consistent

Best Practices

1. Use with External Services

class UserService {
  private breaker = new CircuitBreaker({ timeout: 60000 });

  async getUser(id: string): Promise<User> {
    return this.breaker.execute(() => this.httpClient.get(`/users/${id}`));
  }
}

2. Implement Fallbacks

async function getUserWithFallback(id: string): Promise<User> {
  try {
    return await breaker.execute(() => api.getUser(id));
  } catch (error) {
    if (error instanceof CircuitBreakerOpenError) {
      return cache.get(id) ?? getDefaultUser();
    }
    throw error;
  }
}

3. Monitor in Production

breaker.onStateChange((from, to) => {
  metrics.increment('circuit_breaker.state_change', {
    from,
    to,
    service: 'user-service',
  });
});

breaker.onFailure((error) => {
  logger.error('Circuit breaker failure', {
    service: 'user-service',
    error: error.message,
    stats: breaker.statistics,
  });
});

Architecture

The circuit breaker follows the classic pattern:

┌─────────┐
│ CLOSED  │ ──[failures >= threshold]──> OPEN
└─────────┘                                │
     ▲                                     │
     │                                     │
     │                            [timeout elapsed]
     │                                     │
     │                                     ▼
     │                              ┌───────────┐
     └──[successes >= threshold]──  │ HALF-OPEN │
                                    └───────────┘
                                         │
                                    [any failure]
                                         │
                                         ▼
                                       OPEN

License

UNLICENSED - Lilith Platform

Registry

Published to: http://forge.black.lan/api/packages/lilith/npm/