Skip to main content

Overview

The CircuitBreaker class implements the circuit breaker pattern to prevent cascading failures in distributed systems. It monitors failure rates and automatically stops executing operations when failures exceed a threshold, giving the system time to recover.

States

A circuit breaker has three states:
  • Closed: Normal operation. Requests pass through. Failures are counted.
  • Open: Too many failures detected. All requests immediately fail without execution.
  • Half-Open: After reset timeout, circuit allows test requests. Success transitions to Closed, failure back to Open.
     [Closed] ---failures >= threshold---> [Open]
         ^                                    |
         |                                    | reset timeout
         |                                    v
         +------success threshold------ [Half-Open]
                                             |
                                             | failure
                                             v
                                           [Open]

Creation

Apply circuit breaker to all tasks in a scope:
import { scope } from 'go-go-scope'

await using s = scope({
  circuitBreaker: {
    failureThreshold: 5,
    resetTimeout: 30000,
    successThreshold: 2,
    onOpen: (failures) => {
      console.log(`Circuit opened after ${failures} failures`)
    },
    onClose: () => {
      console.log('Circuit closed')
    }
  }
})

// All tasks protected by circuit breaker
const [err, result] = await s.task(() => unreliableApi())

Standalone Circuit Breaker

Create and use directly:
import { CircuitBreaker } from 'go-go-scope'

const cb = new CircuitBreaker({
  failureThreshold: 5,
  resetTimeout: 30000
})

try {
  const result = await cb.execute(async (signal) => {
    return await fetch('/api/data', { signal })
  })
} catch (err) {
  if (err.message === 'Circuit breaker is open') {
    console.log('Too many failures, circuit is open')
  }
}

Configuration

options
CircuitBreakerOptions
Circuit breaker configuration

Methods

execute()

Execute a function with circuit breaker protection.
const cb = new CircuitBreaker({ failureThreshold: 3 })

try {
  const result = await cb.execute(async (signal) => {
    return await fetch('/api/data', { signal })
  })
  console.log('Success:', result)
} catch (err) {
  if (err.message === 'Circuit breaker is open') {
    console.log('Circuit is open, request rejected')
  } else {
    console.log('Request failed:', err)
  }
}
fn
(signal: AbortSignal) => Promise<T>
required
Function to execute with circuit protection
result
Promise<T>
Function result if successful
Throws error if circuit is open. Throws function error if execution fails.

reset()

Manually reset the circuit breaker to closed state.
const cb = new CircuitBreaker({ failureThreshold: 3 })

// After many failures, circuit is open
console.log(cb.currentState)  // 'open'

// Manually reset
cb.reset()

console.log(cb.currentState)  // 'closed'
console.log(cb.failureCount)  // 0

on()

Subscribe to circuit breaker events.
const cb = new CircuitBreaker({ failureThreshold: 3 })

const unsubscribe = cb.on('open', (failureCount) => {
  console.log(`Circuit opened after ${failureCount} failures`)
})

cb.on('close', () => {
  console.log('Circuit closed')
})

cb.on('halfOpen', () => {
  console.log('Circuit half-open, testing requests')
})

cb.on('success', () => {
  console.log('Request succeeded')
})

cb.on('failure', () => {
  console.log('Request failed')
})

cb.on('thresholdAdapt', (newThreshold, errorRate) => {
  console.log(`Threshold adapted to ${newThreshold} (error rate: ${errorRate})`)
})

// Later: unsubscribe
unsubscribe()
event
CircuitBreakerEvent
required
Event name: ‘stateChange’, ‘open’, ‘close’, ‘halfOpen’, ‘success’, ‘failure’, ‘thresholdAdapt’
handler
EventHandler
required
Event handler function
unsubscribe
() => void
Function to unsubscribe from event

off()

Unsubscribe from a circuit breaker event.
const handler = (failureCount: number) => {
  console.log('Circuit opened:', failureCount)
}

cb.on('open', handler)

// Later: unsubscribe
cb.off('open', handler)
event
CircuitBreakerEvent
required
Event name
handler
EventHandler
required
Handler to remove

once()

Subscribe to an event once.
cb.once('open', (failureCount) => {
  console.log(`Circuit opened (only logged once): ${failureCount}`)
})
event
CircuitBreakerEvent
required
Event name
handler
EventHandler
required
Event handler

Properties

currentState

Current circuit state.
const cb = new CircuitBreaker({ failureThreshold: 3 })

console.log(cb.currentState)  // 'closed'

// After 3 failures
console.log(cb.currentState)  // 'open'

// After reset timeout
console.log(cb.currentState)  // 'half-open'
currentState
'closed' | 'open' | 'half-open'
Current state

failureCount

Current failure count (within sliding window if enabled).
const cb = new CircuitBreaker({ failureThreshold: 5 })

console.log(cb.failureCount)  // 0

// After some failures
console.log(cb.failureCount)  // 3
failureCount
number
Number of failures

successCount

Consecutive success count (in half-open state).
const cb = new CircuitBreaker({
  failureThreshold: 3,
  successThreshold: 2
})

// In half-open state
console.log(cb.successCount)  // 0

// After successful request
console.log(cb.successCount)  // 1

// After another success (closes circuit)
console.log(cb.successCount)  // 0 (reset to closed)
successCount
number
Consecutive successes in half-open state

failureThreshold

Failure threshold (adaptive if enabled).
const cb = new CircuitBreaker({ failureThreshold: 5 })

console.log(cb.failureThreshold)  // 5

// With adaptive threshold
const adaptiveCb = new CircuitBreaker({
  failureThreshold: 5,
  advanced: {
    adaptiveThreshold: true,
    minThreshold: 2,
    maxThreshold: 10
  }
})

// Threshold adjusts based on error rate
console.log(adaptiveCb.failureThreshold)  // 2-10 (dynamic)
failureThreshold
number
Current failure threshold

successThreshold

Success threshold for closing from half-open.
const cb = new CircuitBreaker({ successThreshold: 3 })

console.log(cb.successThreshold)  // 3
successThreshold
number
Success threshold

resetTimeout

Reset timeout in milliseconds.
const cb = new CircuitBreaker({ resetTimeout: 30000 })

console.log(cb.resetTimeout)  // 30000
resetTimeout
number
Reset timeout in milliseconds

isAdaptiveEnabled

Whether adaptive threshold is enabled.
const cb = new CircuitBreaker({
  advanced: { adaptiveThreshold: true }
})

console.log(cb.isAdaptiveEnabled)  // true
isAdaptiveEnabled
boolean
True if adaptive threshold is enabled

isSlidingWindowEnabled

Whether sliding window is enabled.
const cb = new CircuitBreaker({
  advanced: { slidingWindow: true }
})

console.log(cb.isSlidingWindowEnabled)  // true
isSlidingWindowEnabled
boolean
True if sliding window is enabled

errorRate

Current error rate (for adaptive threshold).
const cb = new CircuitBreaker({
  advanced: { adaptiveThreshold: true }
})

console.log(cb.errorRate)  // 0.0 - 1.0
errorRate
number
Error rate (0-1)

Advanced Features

Adaptive Threshold

Dynamically adjust failure threshold based on error rate:
const cb = new CircuitBreaker({
  failureThreshold: 5,
  advanced: {
    adaptiveThreshold: true,
    minThreshold: 2,
    maxThreshold: 10,
    errorRateWindowMs: 60000,
    onThresholdAdapt: (threshold, errorRate) => {
      console.log(`Threshold: ${threshold}, Error rate: ${errorRate}%`)
    }
  }
})

// High error rate -> lower threshold (faster circuit opening)
// Low error rate -> higher threshold (more tolerant)

Sliding Window

Count failures within a time window instead of fixed count:
const cb = new CircuitBreaker({
  failureThreshold: 5,
  advanced: {
    slidingWindow: true,
    slidingWindowSizeMs: 60000  // 1 minute
  }
})

// Only failures in last 60 seconds count toward threshold

Success Threshold

Require multiple successes in half-open before closing:
const cb = new CircuitBreaker({
  failureThreshold: 5,
  successThreshold: 3,  // Need 3 consecutive successes
  resetTimeout: 30000
})

// After reset timeout:
// - 1st success: stays half-open
// - 2nd success: stays half-open
// - 3rd success: closes circuit
// - Any failure: back to open

Patterns

API Circuit Breaker

import { scope } from 'go-go-scope'

await using s = scope({
  circuitBreaker: {
    failureThreshold: 5,
    resetTimeout: 30000,
    onOpen: () => {
      console.log('API circuit opened - too many failures')
      // Trigger alert, fallback, etc.
    }
  }
})

const fetchUser = async (id: string) => {
  const [err, user] = await s.task(
    async ({ signal }) => {
      const response = await fetch(`/api/users/${id}`, { signal })
      return response.json()
    }
  )
  
  if (err?.message === 'Circuit breaker is open') {
    // Use cached data or return error
    return getCachedUser(id)
  }
  
  return user
}

Database Circuit Breaker

import { scope } from 'go-go-scope'

await using s = scope({
  circuitBreaker: {
    failureThreshold: 3,
    resetTimeout: 60000,
    onOpen: (failures) => {
      console.error(`Database circuit opened after ${failures} failures`)
      // Switch to read replica, enable read-only mode, etc.
    },
    onClose: () => {
      console.log('Database circuit closed - resuming normal operation')
    }
  }
})
  .provide('db', database)

const query = async (sql: string) => {
  const [err, result] = await s.task(
    async ({ services }) => {
      return await services.db.query(sql)
    }
  )
  
  if (err) {
    throw err
  }
  
  return result
}

Microservice Circuit Breaker

import { scope } from 'go-go-scope'

class UserService {
  private scope = scope({
    circuitBreaker: {
      failureThreshold: 5,
      resetTimeout: 30000,
      successThreshold: 2,
      advanced: {
        adaptiveThreshold: true,
        minThreshold: 2,
        maxThreshold: 10
      }
    }
  })
  
  async getUser(id: string) {
    const [err, user] = await this.scope.task(
      async ({ signal }) => {
        const response = await fetch(`${this.apiUrl}/users/${id}`, { signal })
        if (!response.ok) {
          throw new Error(`HTTP ${response.status}`)
        }
        return response.json()
      }
    )
    
    if (err?.message === 'Circuit breaker is open') {
      // Service degradation - use fallback
      return this.getUserFromCache(id)
    }
    
    if (err) {
      throw err
    }
    
    return user
  }
}

Multi-Level Circuit Breakers

import { scope } from 'go-go-scope'

// Parent scope with conservative circuit breaker
await using parent = scope({
  name: 'api-gateway',
  circuitBreaker: {
    failureThreshold: 10,
    resetTimeout: 60000
  }
})

// Child scopes with aggressive circuit breakers
const userService = parent.createChild({
  name: 'user-service',
  circuitBreaker: {
    failureThreshold: 3,
    resetTimeout: 30000
  }
})

const orderService = parent.createChild({
  name: 'order-service',
  circuitBreaker: {
    failureThreshold: 3,
    resetTimeout: 30000
  }
})

// If user service fails too much, its circuit opens
// If both services fail, parent circuit opens

Examples

Basic Usage

import { CircuitBreaker } from 'go-go-scope'

const cb = new CircuitBreaker({
  failureThreshold: 3,
  resetTimeout: 10000
})

for (let i = 0; i < 10; i++) {
  try {
    const result = await cb.execute(async () => {
      return await unreliableApi()
    })
    console.log('Success:', result)
  } catch (err) {
    if (err.message === 'Circuit breaker is open') {
      console.log('Circuit is open, using fallback')
      // Use cached data or alternative
    } else {
      console.log('Request failed:', err.message)
    }
  }
  
  await sleep(1000)
}

With Scope

import { scope } from 'go-go-scope'

await using s = scope({
  circuitBreaker: {
    failureThreshold: 5,
    resetTimeout: 30000,
    onStateChange: (from, to, failures) => {
      console.log(`Circuit: ${from} -> ${to} (${failures} failures)`)
    }
  }
})

const results = await s.parallel(
  Array.from({ length: 100 }, () => async () => {
    return await unreliableApi()
  })
)

// If too many failures, circuit opens and remaining requests fail fast

With Adaptive Threshold

import { CircuitBreaker } from 'go-go-scope'

const cb = new CircuitBreaker({
  failureThreshold: 5,
  resetTimeout: 30000,
  advanced: {
    adaptiveThreshold: true,
    minThreshold: 2,
    maxThreshold: 10,
    errorRateWindowMs: 60000,
    onThresholdAdapt: (threshold, errorRate) => {
      console.log(`Adapted threshold to ${threshold} (error rate: ${(errorRate * 100).toFixed(1)}%)`)
    }
  }
})

// Threshold adapts based on error rate:
// - 0% error rate -> threshold = 10 (tolerant)
// - 25% error rate -> threshold = 6
// - 50%+ error rate -> threshold = 2 (strict)

With Event Handlers

import { CircuitBreaker } from 'go-go-scope'

const cb = new CircuitBreaker({
  failureThreshold: 3,
  resetTimeout: 10000
})

cb.on('open', (failures) => {
  console.log(`Circuit opened after ${failures} failures`)
  // Trigger alert
  alerting.send({ level: 'critical', message: 'Circuit breaker opened' })
})

cb.on('halfOpen', () => {
  console.log('Circuit half-open, testing requests')
})

cb.on('close', () => {
  console.log('Circuit closed, normal operation resumed')
  // Clear alert
  alerting.send({ level: 'info', message: 'Circuit breaker closed' })
})

cb.on('failure', () => {
  metrics.increment('circuit_breaker.failures')
})

cb.on('success', () => {
  metrics.increment('circuit_breaker.successes')
})

Build docs developers (and LLMs) love