Skip to content

Circuit Breaker States — Complete Implementation Guide

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Circuit Breaker States. We cover key concepts, practical examples, and best practices to help you master this topic.

Circuit breaker states form a state machine with three states: closed (normal), open (failing), and half-open (testing recovery), each with specific transition rules and behaviors.

What You'll Learn

By the end of this tutorial, you will understand the three circuit breaker states, the transition rules between them, and how to configure thresholds for each transition.

Why It Matters

Understanding state transitions is essential for correct circuit breaker implementation. Wrong transitions cause either false positives or slow failure detection.

Real-World Use

DodaTech's circuit breakers use configurable state transitions: closed to open after 5 failures, open to half-open after 30 seconds, and half-open to closed after 1 successful probe.

State Machine Learning Path

flowchart LR
  A[Circuit Intro] --> B[States]
  B --> C[Closed]
  B --> D[Open]
  B --> E[Half-Open]
  B --> F{You Are Here}
  style F fill:#f90,color:#fff

The Three States

The circuit breaker state machine has clear transition rules:

              failures >= threshold
  CLOSED ──────────────────────────> OPEN
    ^                                  │
    │                                  │ timeout expires
    │                                  v
    +────────────── CLOSED <────── HALF-OPEN
                       1 success
const STATES = {
  CLOSED: "closed",
  OPEN: "open",
  HALF_OPEN: "half-open"
};

class StateMachine {
  constructor(options) {
    this.state = STATES.CLOSED;
    this.threshold = options.threshold || 5;
    this.resetTimeout = options.resetTimeout || 30000;
    this.failureCount = 0;
    this.nextAttempt = Date.now();
    this.listeners = [];
  }

  getState() { return this.state; }

  onTransition(listener) {
    this.listeners.push(listener);
  }

  transition(newState) {
    const oldState = this.state;
    this.state = newState;
    this.listeners.forEach(l => l({ from: oldState, to: newState }));
    console.log(`State: ${oldState} -> ${newState}`);
  }

  async call(fn) {
    switch (this.state) {
      case STATES.OPEN:
        return this.handleOpen(fn);
      case STATES.HALF_OPEN:
        return this.handleHalfOpen(fn);
      case STATES.CLOSED:
        return this.handleClosed(fn);
    }
  }

  async handleClosed(fn) {
    try {
      const result = await fn();
      this.failureCount = 0;
      return result;
    } catch (err) {
      this.failureCount++;
      if (this.failureCount >= this.threshold) {
        this.transition(STATES.OPEN);
        this.nextAttempt = Date.now() + this.resetTimeout;
      }
      throw err;
    }
  }

  async handleOpen(fn) {
    if (Date.now() >= this.nextAttempt) {
      this.transition(STATES.HALF_OPEN);
      return this.handleHalfOpen(fn);
    }
    throw new Error("Circuit breaker is open");
  }

  async handleHalfOpen(fn) {
    try {
      const result = await fn();
      this.transition(STATES.CLOSED);
      this.failureCount = 0;
      return result;
    } catch (err) {
      this.transition(STATES.OPEN);
      this.nextAttempt = Date.now() + this.resetTimeout;
      throw err;
    }
  }
}

State Transition Configuration

Each transition has configurable parameters that affect system resilience.

Transition Parameter Default Effect
Closed -> Open failureThreshold 5 Higher values delay protection
Open -> Half-Open resetTimeout 30000ms Shorter values probe more often
Half-Open -> Closed successThreshold 1 Higher values require more probes
Half-Open -> Open failureCount 1 Single failure reopens immediately

Monitoring State Changes

Each state change should be logged and monitored to detect patterns.

class MonitoredStateMachine extends StateMachine {
  constructor(options) {
    super(options);
    this.stateHistory = [];
    this.onTransition((event) => {
      this.stateHistory.push({
        ...event,
        time: new Date().toISOString(),
        failureCount: this.failureCount
      });
    });
  }

  getReport() {
    const recent = this.stateHistory.slice(-50);
    const openCount = recent.filter(e => e.to === "open").length;

    return {
      currentState: this.state,
      failureCount: this.failureCount,
      transitionsLast50: recent.length,
      openEventsLast50: openCount,
      lastTransition: recent[recent.length - 1] || null,
      nextAttempt: new Date(this.nextAttempt).toISOString()
    };
  }
}

Common Mistakes

  1. Direct open-to-closed transition -- Never transition from open directly to closed. Always probe through half-open first.

  2. Counting half-open failures against the closed threshold -- Half-open failures should immediately reopen the circuit, not increment the closed counter.

  3. Not resetting failure count on close -- When transitioning half-open to closed, reset the failure count to zero.

  4. Using the same timeout for all services -- Critical services may need shorter timeouts. Non-critical services can wait longer.

  5. Opening circuit on first failure -- A single failure should not open the circuit. Use a threshold to distinguish transient from persistent problems.

Practice Questions

  1. What triggers the transition from closed to open? The failure count reaches the threshold.

  2. Why does half-open exist as a state? To probe whether the downstream service has recovered before fully closing the circuit.

  3. What happens in half-open if the probe fails? The circuit immediately reopens and the reset timer restarts.

  4. Challenge: Implement a state history viewer that shows transitions over time.

class StateHistory {
  constructor(maxEntries = 100) {
    this.entries = [];
    this.maxEntries = maxEntries;
  }

  record(event) {
    this.entries.push(event);
    if (this.entries.length > this.maxEntries) this.entries.shift();
  }

  visualize() {
    return this.entries.map(e =>
      `${e.time}: ${e.from} -> ${e.to} (failures: ${e.failureCount})`
    ).join("\n");
  }
}

FAQ

How many probes should half-open send?

One probe is standard. If it succeeds, close the circuit. If it fails, reopen. Some implementations use multiple probes for higher confidence.

Should the circuit stay open indefinitely?

No. Use a reset timeout. If the service is permanently down, monitor and alert instead of keeping the circuit open forever.

What if a probe succeeds in half-open but then the next request fails?

The circuit closes after the probe, then reopens on the next failure. This is normal behavior.

Can the circuit skip half-open and go directly to closed?

No. Half-open is essential for safe recovery testing. Skipping it risks sending traffic to an unhealthy service.

How do I handle rapid open/close cycling?

Use exponential backoff on the reset timeout. If the circuit keeps reopening, double the timeout each time.

Mini Project

Build a state machine-driven circuit breaker with visualization, history tracking, and configurable transition rules.

class FullStateCircuitBreaker {
  constructor(opts = {}) {
    this.states = { closed: "closed", open: "open", halfOpen: "half-open" };
    this.state = this.states.closed;
    this.failureThreshold = opts.failureThreshold || 5;
    this.successThreshold = opts.successThreshold || 1;
    this.resetTimeout = opts.resetTimeout || 30000;
    this.failureCount = 0;
    this.successCount = 0;
    this.nextAttempt = Date.now();
    this.history = [];
  }

  async call(fn) {
    if (this.state === this.states.open) {
      if (Date.now() < this.nextAttempt) {
        throw new Error("Circuit open");
      }
      this.changeState(this.states.halfOpen);
    }

    if (this.state === this.states.halfOpen) {
      return this.halfOpenCall(fn);
    }

    return this.closedCall(fn);
  }

  async closedCall(fn) {
    try {
      const r = await fn();
      this.failureCount = 0;
      return r;
    } catch (err) {
      this.failureCount++;
      if (this.failureCount >= this.failureThreshold) {
        this.changeState(this.states.open);
        this.nextAttempt = Date.now() + this.resetTimeout;
      }
      throw err;
    }
  }

  async halfOpenCall(fn) {
    try {
      const r = await fn();
      this.successCount++;
      if (this.successCount >= this.successThreshold) {
        this.changeState(this.states.closed);
        this.failureCount = 0;
        this.successCount = 0;
      }
      return r;
    } catch (err) {
      this.changeState(this.states.open);
      this.nextAttempt = Date.now() + this.resetTimeout;
      this.successCount = 0;
      throw err;
    }
  }

  changeState(newState) {
    this.history.push({
      from: this.state,
      to: newState,
      time: new Date().toISOString()
    });
    this.state = newState;
  }
}

What's Next

Now that you understand circuit breaker states, explore implementing circuit breakers in Node.js. Then learn about configuring failure thresholds.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro