Circuit Breaker Implementation — Complete Node.js Guide
In this tutorial, you will learn about Circuit Breaker Implementation. We cover key concepts, practical examples, and best practices to help you master this topic.
Circuit breaker implementation in Node.js involves building a class that wraps function calls, tracks successes and failures, manages state transitions, and integrates with Express middleware.
What You'll Learn
By the end of this tutorial, you will implement a production-ready circuit breaker in Node.js with event emitters, configurable thresholds, and Express middleware integration.
Why It Matters
A well-implemented circuit breaker is reusable across all service calls. DodaTech's Node.js services share a common circuit breaker implementation with consistent behavior.
Real-World Use
DodaTech's Node.js API Gateway uses circuit breakers around every downstream service call with configurable thresholds per service.
Implementation Learning Path
flowchart LR
A[States] --> B[Implementation]
B --> C[Circuit Breaker Class]
C --> D[Express Middleware]
B --> E{You Are Here}
style E fill:#f90,color:#fff
Basic Circuit Breaker Class
A reusable circuit breaker class that wraps async functions with state management.
const EventEmitter = require("events");
class CircuitBreaker extends EventEmitter {
constructor(options = {}) {
super();
this.threshold = options.threshold || 5;
this.resetTimeout = options.resetTimeout || 30000;
this.state = "closed";
this.failureCount = 0;
this.nextAttempt = Date.now();
}
async call(fn) {
if (this.state === "open") {
if (Date.now() < this.nextAttempt) {
this.emit("reject", { time: Date.now() });
throw new Error("CircuitBreakerOpen");
}
this.transition("half-open");
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (err) {
this.onFailure(err);
throw err;
}
}
onSuccess() {
this.failureCount = 0;
if (this.state === "half-open") {
this.transition("closed");
}
}
onFailure(err) {
this.failureCount++;
this.emit("failure", { count: this.failureCount, error: err.message });
if (this.state === "half-open" || this.failureCount >= this.threshold) {
this.transition("open");
this.nextAttempt = Date.now() + this.resetTimeout;
}
}
transition(state) {
const prev = this.state;
this.state = state;
this.emit("transition", { from: prev, to: state, time: Date.now() });
}
}
module.exports = CircuitBreaker;
Express Middleware Integration
Wrap Express route handlers with circuit breaker protection for downstream HTTP calls.
const express = require("express");
const CircuitBreaker = require("./circuit-breaker");
const app = express();
const downstreamCB = new CircuitBreaker({ threshold: 3, resetTimeout: 15000 });
downstreamCB.on("transition", (event) => {
console.log(`Downstream service: ${event.from} -> ${event.to}`);
});
downstreamCB.on("reject", () => {
console.warn("Request rejected: downstream circuit is open");
});
async function callDownstreamService() {
const response = await fetch("http://downstream-api/data");
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
async function circuitBreakingHandler(req, res, next) {
try {
const data = await downstreamCB.call(callDownstreamService);
res.json(data);
} catch (err) {
if (err.message === "CircuitBreakerOpen") {
return res.status(503).json({
error: "Service temporarily unavailable",
retryAfter: Math.ceil((downstreamCB.nextAttempt - Date.now()) / 1000)
});
}
next(err);
}
}
app.get("/api/data", circuitBreakingHandler);
app.listen(3000);
Per-Endpoint Circuit Breakers
Different endpoints may share the same downstream service but need independent circuit breakers for different operations.
class CircuitBreakerRegistry {
constructor() {
this.breakers = new Map();
}
get(name, options = {}) {
if (!this.breakers.has(name)) {
this.breakers.set(name, new CircuitBreaker(options));
}
return this.breakers.get(name);
}
getAllStates() {
const states = {};
for (const [name, cb] of this.breakers) {
states[name] = { state: cb.state, failures: cb.failureCount };
}
return states;
}
}
const registry = new CircuitBreakerRegistry();
app.get("/api/users", async (req, res) => {
const cb = registry.get("user-service", { threshold: 5 });
try {
const users = await cb.call(() => fetchUsers());
res.json(users);
} catch (err) {
if (err.message === "CircuitBreakerOpen") {
return res.status(503).json({ error: "User service unavailable" });
}
res.status(500).json({ error: err.message });
}
});
app.get("/api/orders", async (req, res) => {
const cb = registry.get("order-service", { threshold: 3 });
// Similar pattern
});
Common Mistakes
Not emitting events -- Without events, you cannot monitor circuit breaker state. Always emit state change events.
Creating a new circuit breaker per request -- Circuit breakers must persist across requests to track failure history.
Not handling half-open failures correctly -- A half-open failure should reopen immediately, not increment the closed threshold.
Blocking the event loop -- Circuit breaker decision logic is synchronous and fast. Keep it that way.
Forgetting to reset failure count on close -- When recovering from half-open to closed, failure count must reset to zero.
Practice Questions
Why use EventEmitter for circuit breaker notifications? It allows decoupled monitoring, logging, and alerting without modifying the circuit breaker code.
How do per-endpoint circuit breakers share state? A registry manages named circuit breaker instances. Each endpoint accesses its own instance.
What happens to in-flight requests when the circuit opens? They continue running. Only new requests are rejected. The circuit breaker tracks failures from completed requests.
Challenge: Implement a circuit breaker that falls back to cached data when open.
async function callWithCachedFallback(cb, fn, cacheKey, cacheStore) {
try {
const result = await cb.call(fn);
cacheStore.set(cacheKey, result);
return result;
} catch (err) {
if (err.message === "CircuitBreakerOpen") {
return cacheStore.get(cacheKey) || { error: "No cached data" };
}
throw err;
}
}
FAQ
Mini Project
Build a complete circuit breaker implementation with event emitter, registry, Express middleware, and health check endpoint.
const express = require("express");
const EventEmitter = require("events");
class CircuitBreaker extends EventEmitter {
constructor(name, options = {}) {
super();
this.name = name;
this.threshold = options.threshold || 5;
this.resetTimeout = options.resetTimeout || 30000;
this.state = "closed";
this.failureCount = 0;
this.successCount = 0;
this.nextAttempt = Date.now();
}
async call(fn) {
if (this.state === "open") {
if (Date.now() < this.nextAttempt) {
this.emit("rejected", { name: this.name });
throw new Error("CircuitBreakerOpen");
}
this.changeState("half-open");
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (err) {
this.onFailure(err);
throw err;
}
}
onSuccess() {
this.failureCount = 0;
this.successCount++;
if (this.state === "half-open") {
this.changeState("closed");
}
}
onFailure(err) {
this.failureCount++;
this.successCount = 0;
this.emit("failure", { name: this.name, count: this.failureCount, error: err.message });
if (this.state === "half-open" || this.failureCount >= this.threshold) {
this.changeState("open");
this.nextAttempt = Date.now() + this.resetTimeout;
}
}
changeState(state) {
const prev = this.state;
this.state = state;
this.emit("stateChange", { name: this.name, from: prev, to: state });
}
getStatus() {
return {
name: this.name,
state: this.state,
failures: this.failureCount,
threshold: this.threshold,
nextRetry: new Date(this.nextAttempt).toISOString()
};
}
}
module.exports = CircuitBreaker;
What's Next
Now that you understand circuit breaker implementation, explore configuring failure thresholds. Then learn about configuring half-open probe behavior.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro