The most surprising thing about circuit breakers is that they don’t just prevent failures; they actively improve system resilience by forcing you to confront and fix your dependencies, rather than just hoping they don’t break.
Let’s see how this plays out. Imagine we have a service that needs to fetch user data from a userService.
// userService.js
async function getUser(userId) {
// Simulate network latency and potential failure
await new Promise(resolve => setTimeout(resolve, Math.random() * 1000));
if (Math.random() > 0.7) {
throw new Error('User service unavailable');
}
return { id: userId, name: 'Alice' };
}
module.exports = { getUser };
Our main application directly calls this:
// app.js (without circuit breaker)
const express = require('express');
const { getUser } = require('./userService');
const app = express();
app.get('/user/:id', async (req, res) => {
try {
const user = await getUser(req.params.id);
res.json(user);
} catch (error) {
console.error('Failed to get user:', error.message);
res.status(500).send('Error fetching user data');
}
});
app.listen(3000, () => console.log('App listening on port 3000'));
If userService starts failing frequently, our app will also start returning 500 errors, potentially overwhelming the userService further and cascading the failure.
Now, let’s introduce opossum. First, install it:
npm install opossum
Here’s how we wrap our getUser function with a circuit breaker:
// app.js (with circuit breaker)
const express = require('express');
const opossum = require('opossum');
const { getUser: getUserOriginal } = require('./userService');
const app = express();
// Configure the circuit breaker
const getUser = new opossum(getUserOriginal, {
// How many failures trigger the circuit breaker to open
failureThreshold: 5,
// How long to wait before attempting a half-open transition
resetTimeout: 30000, // 30 seconds
// How many successful calls in half-open state to close the circuit
successThreshold: 3,
// Name for logging and identification
name: 'getUserCircuitBreaker'
});
// Optional: Listen for state changes
getUser.on('open', () => {
console.warn('getUserCircuitBreaker: OPEN');
});
getUser.on('halfOpen', () => {
console.warn('getUserCircuitBreaker: HALF_OPEN');
});
getUser.on('close', () => {
console.log('getUserCircuitBreaker: CLOSED');
});
getUser.on('reject', (error) => {
console.error(`getUserCircuitBreaker: REJECTED - ${error.message}`);
});
getUser.on('fallback', (error) => {
console.warn(`getUserCircuitBreaker: FALLBACK - ${error.message}`);
});
app.get('/user/:id', async (req, res) => {
try {
const user = await getUser(req.params.id);
res.json(user);
} catch (error) {
// If the circuit breaker rejected the request immediately (open state)
// or if the underlying function failed and the breaker is still open,
// opossum will throw an error. We can provide a fallback.
console.error('Application error:', error.message);
res.status(503).send('Service temporarily unavailable');
}
});
app.listen(3000, () => console.log('App listening on port 3000'));
The opossum constructor takes your original function and a configuration object.
failureThreshold: This is the number of consecutive failures that will cause the circuit breaker to trip (open). We’ve set it to 5. IfgetUserOriginalthrows an error 5 times in a row, the breaker opens.resetTimeout: Once open, the breaker waits for this duration before transitioning to a "half-open" state. We’ve set it to 30 seconds.successThreshold: In the "half-open" state, the breaker allows a few requests through. If a configured number of these requests succeed, the breaker closes. We’ve set it to 3.name: A helpful identifier for logging and debugging.
The getUser variable is now our protected function. When you call await getUser(userId), opossum intercepts it.
- Closed State: If the breaker is closed,
opossumcallsgetUserOriginal. If it succeeds, all good. If it fails,opossumcounts the failure. If failures reachfailureThreshold, the breaker opens. - Open State: If the breaker is open,
opossumimmediately rejects the request without callinggetUserOriginal. This prevents you from hammering a broken service. AfterresetTimeouthas passed, it transitions to half-open. - Half-Open State:
opossumallows a limited number of requests through togetUserOriginal. If any of these succeed, the breaker closes. If any fail, it immediately re-opens and restarts theresetTimeout. If a specific number of successes (successThreshold) are achieved, the breaker closes.
When opossum rejects a request because the circuit is open, it throws an Error with the message Circuit breaker is open. This is what your catch block will handle. You can then send a 503 Service Temporarily Unavailable response.
The most powerful aspect is how it forces you to think about fallback behavior. When the getUser circuit is open, opossum will reject the request. Your application logic in the catch block is then responsible for providing an alternative. This could be returning cached data, a default response, or simply informing the user that the service is temporarily unavailable. This proactive rejection, rather than a slow, eventual timeout, is key to preventing cascading failures.
The next logical step is to implement more sophisticated fallback strategies, such as returning cached data when the circuit is open, or using a separate, more resilient data source for critical information.