Implementing Circuit Breaker Pattern in Node.js Microservices
Introduction
In distributed systems and microservices architecture, service failures are inevitable. A single failing service can cascade and bring down your entire system if not handled properly. This is where the Circuit Breaker pattern comes to the rescue.
The Circuit Breaker pattern prevents an application from repeatedly trying to execute an operation that's likely to fail, allowing it to continue without waiting for the fault to be fixed or wasting CPU cycles.
Understanding the Circuit Breaker Pattern
The Circuit Breaker works like an electrical circuit breaker in your home. It has three states:
- Closed: Normal operation, requests pass through
- Open: Service is failing, requests are immediately rejected
- Half-Open: Testing phase to see if the service has recovered
When the failure threshold is reached, the circuit breaker trips to the Open state. After a timeout period, it moves to Half-Open to test if the service has recovered.
Building a Custom Circuit Breaker
Let's implement a simple but effective Circuit Breaker class:
class CircuitBreaker {
constructor(request, options = {}) {
this.request = request;
this.state = 'CLOSED';
this.failureCount = 0;
this.successCount = 0;
this.nextAttempt = Date.now();
// Configuration options
this.failureThreshold = options.failureThreshold || 5;
this.successThreshold = options.successThreshold || 2;
this.timeout = options.timeout || 60000; // 60 seconds
this.resetTimeout = options.resetTimeout || 30000; // 30 seconds
}
async call(...args) {
if (this.state === 'OPEN') {
if (this.nextAttempt <= Date.now()) {
this.state = 'HALF_OPEN';
this.successCount = 0;
} else {
throw new Error('Circuit breaker is OPEN');
}
}
try {
const result = await Promise.race([
this.request(...args),
this.timeoutPromise()
]);
return this.onSuccess(result);
} catch (error) {
return this.onFailure(error);
}
}
onSuccess(result) {
this.failureCount = 0;
if (this.state === 'HALF_OPEN') {
this.successCount++;
if (this.successCount >= this.successThreshold) {
this.state = 'CLOSED';
this.successCount = 0;
}
}
return result;
}
onFailure(error) {
this.failureCount++;
if (this.failureCount >= this.failureThreshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.resetTimeout;
}
throw error;
}
timeoutPromise() {
return new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('Request timeout'));
}, this.timeout);
});
}
getState() {
return {
state: this.state,
failureCount: this.failureCount,
successCount: this.successCount,
nextAttempt: this.nextAttempt
};
}
}Using the Circuit Breaker in Practice
Here's how to implement the circuit breaker with an HTTP service call:
const axios = require('axios');
// Define your service call
const callUserService = async (userId) => {
const response = await axios.get(`https://api.example.com/users/${userId}`);
return response.data;
};
// Create circuit breaker instance
const userServiceBreaker = new CircuitBreaker(callUserService, {
failureThreshold: 3,
successThreshold: 2,
timeout: 5000,
resetTimeout: 20000
});
// Express route with circuit breaker
app.get('/users/:id', async (req, res) => {
try {
const user = await userServiceBreaker.call(req.params.id);
res.json(user);
} catch (error) {
if (error.message === 'Circuit breaker is OPEN') {
return res.status(503).json({
error: 'User service is temporarily unavailable',
fallback: 'Using cached data or default response'
});
}
res.status(500).json({ error: 'Internal server error' });
}
});Advanced Features
For production use, consider adding these enhancements:
Fallback Mechanisms
class CircuitBreakerWithFallback extends CircuitBreaker {
constructor(request, fallback, options = {}) {
super(request, options);
this.fallback = fallback;
}
async call(...args) {
try {
return await super.call(...args);
} catch (error) {
if (this.state === 'OPEN' && this.fallback) {
console.log('Circuit breaker OPEN, executing fallback');
return await this.fallback(...args);
}
throw error;
}
}
}Metrics and Monitoring
class MonitoredCircuitBreaker extends CircuitBreaker {
constructor(request, options = {}) {
super(request, options);
this.metrics = {
totalRequests: 0,
successfulRequests: 0,
failedRequests: 0,
circuitOpenCount: 0
};
}
async call(...args) {
this.metrics.totalRequests++;
try {
const result = await super.call(...args);
this.metrics.successfulRequests++;
return result;
} catch (error) {
this.metrics.failedRequests++;
if (this.state === 'OPEN') {
this.metrics.circuitOpenCount++;
}
throw error;
}
}
getMetrics() {
return {
...this.metrics,
successRate: this.metrics.totalRequests > 0
? (this.metrics.successfulRequests / this.metrics.totalRequests) * 100
: 0
};
}
}Production Considerations
When implementing circuit breakers in production:
- Choose appropriate thresholds: Start conservative and adjust based on your service's behavior
- Implement proper logging: Track state changes and failure patterns
- Use existing libraries: Consider
opossumor similar battle-tested libraries for production - Health checks: Combine with health check endpoints for better observability
- Gradual recovery: Implement gradual traffic restoration in half-open state
Conclusion
The Circuit Breaker pattern is essential for building resilient microservices. It prevents cascading failures, improves system stability, and provides better user experience during service disruptions. While this implementation covers the basics, production systems should use mature libraries that offer additional features like bulkheads, rate limiting, and comprehensive monitoring.
Remember, resilience patterns work best when combined. Consider implementing circuit breakers alongside retry mechanisms, timeouts, and bulkheads for comprehensive fault tolerance.
Related Posts
Building Event-Driven Microservices with Node.js and RabbitMQ
Learn how to design resilient microservices using event-driven architecture with practical Node.js and RabbitMQ examples.
Building Scalable Event-Driven Architecture with Message Queues and Event Sourcing
Learn how to design resilient, scalable systems using event-driven patterns, message queues, and event sourcing principles.
Building Scalable Microservices: From Monolith to Distributed Architecture
Learn how to break down monolithic applications into scalable microservices with practical patterns and real-world examples.