Circuit Breaker States — Complete Implementation Guide
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
Direct open-to-closed transition -- Never transition from open directly to closed. Always probe through half-open first.
Counting half-open failures against the closed threshold -- Half-open failures should immediately reopen the circuit, not increment the closed counter.
Not resetting failure count on close -- When transitioning half-open to closed, reset the failure count to zero.
Using the same timeout for all services -- Critical services may need shorter timeouts. Non-critical services can wait longer.
Opening circuit on first failure -- A single failure should not open the circuit. Use a threshold to distinguish transient from persistent problems.
Practice Questions
What triggers the transition from closed to open? The failure count reaches the threshold.
Why does half-open exist as a state? To probe whether the downstream service has recovered before fully closing the circuit.
What happens in half-open if the probe fails? The circuit immediately reopens and the reset timer restarts.
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
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