Circuit Breaker Pattern in Python, JavaScript, and Java
Prevent cascading failures by stopping requests to failing services. An architectural pattern for resilient distributed systems.
Overview
The Circuit Breaker pattern wraps calls to a remote dependency so your application stops hammering a service that is already failing. When failures cross a threshold, the breaker “trips” and calls fail fast instead of timing out one by one. After a cooldown, a few probe calls decide whether the dependency recovered.
This prevents two things at once: cascading failures through your own services, and resource exhaustion from threads piling up on a dead dependency — a classic failure mode in distributed systems.
When to Use
Use a circuit breaker when:
- A service call can fail or hang and you can’t afford to block on every attempt
- You need graceful degradation: cached data, a default response, or a queued retry later
- You want to give a struggling dependency room to recover instead of drowning it in retries
Skip it when the call is in-process (no network boundary), when failures must surface to the caller immediately, or when the dependency is so critical that every request legitimately needs it — in that case you want timeouts plus retry, and a circuit breaker on top only if the downstream can be overwhelmed.
Solution
Python
import time
from enum import Enum, auto
class CircuitState(Enum):
CLOSED = auto() # Normal operation
OPEN = auto() # Failing fast
HALF_OPEN = auto() # Testing recovery
class CircuitBreaker:
def __init__(self, failure_threshold=3, recovery_timeout=5):
self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout
self.state = CircuitState.CLOSED
self.failures = 0
self.last_failure_time = None
def call(self, func, *args, **kwargs):
if self.state == CircuitState.OPEN:
if time.time() - self.last_failure_time >= self.recovery_timeout:
self.state = CircuitState.HALF_OPEN
else:
raise Exception("Circuit breaker is OPEN")
try:
result = func(*args, **kwargs)
self._on_success()
return result
except Exception as e:
self._on_failure()
raise e
def _on_success(self):
self.failures = 0
self.state = CircuitState.CLOSED
def _on_failure(self):
self.failures += 1
self.last_failure_time = time.time()
if self.failures >= self.failure_threshold:
self.state = CircuitState.OPEN
# Usage
def fetch_data():
# Simulated flaky service
import random
if random.random() < 0.7:
raise Exception("Service error")
return "Data"
breaker = CircuitBreaker(failure_threshold=2, recovery_timeout=3)
for i in range(5):
try:
print(breaker.call(fetch_data))
except Exception as e:
print(f"Call {i+1}: {e}")
JavaScript
class CircuitBreaker {
constructor(failureThreshold = 3, recoveryTimeout = 5000) {
this.failureThreshold = failureThreshold;
this.recoveryTimeout = recoveryTimeout;
this.state = 'CLOSED';
this.failures = 0;
this.lastFailureTime = null;
}
async call(fn) {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime >= this.recoveryTimeout) {
this.state = 'HALF_OPEN';
} else {
throw new Error('Circuit breaker is OPEN');
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (e) {
this.onFailure();
throw e;
}
}
onSuccess() {
this.failures = 0;
this.state = 'CLOSED';
}
onFailure() {
this.failures++;
this.lastFailureTime = Date.now();
if (this.failures >= this.failureThreshold) {
this.state = 'OPEN';
}
}
}
// Usage
async function fetchData() {
if (Math.random() < 0.7) throw new Error('Service error');
return 'Data';
}
const breaker = new CircuitBreaker(2, 3000);
(async () => {
for (let i = 0; i < 5; i++) {
try {
const result = await breaker.call(fetchData);
console.log(`Call ${i + 1}:`, result);
} catch (e) {
console.log(`Call ${i + 1}:`, e.message);
}
}
})();
Java
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
public class CircuitBreaker {
private enum State { CLOSED, OPEN, HALF_OPEN }
private final int failureThreshold;
private final long recoveryTimeoutMs;
private State state = State.CLOSED;
private final AtomicInteger failures = new AtomicInteger(0);
private volatile long lastFailureTime = 0;
public CircuitBreaker(int failureThreshold, long recoveryTimeoutMs) {
this.failureThreshold = failureThreshold;
this.recoveryTimeoutMs = recoveryTimeoutMs;
}
public <T> T call(Supplier<T> supplier) throws Exception {
if (state == State.OPEN) {
if (System.currentTimeMillis() - lastFailureTime >= recoveryTimeoutMs) {
state = State.HALF_OPEN;
} else {
throw new Exception("Circuit breaker is OPEN");
}
}
try {
T result = supplier.get();
onSuccess();
return result;
} catch (Exception e) {
onFailure();
throw e;
}
}
private void onSuccess() {
failures.set(0);
state = State.CLOSED;
}
private void onFailure() {
int count = failures.incrementAndGet();
lastFailureTime = System.currentTimeMillis();
if (count >= failureThreshold) {
state = State.OPEN;
}
}
}
// Usage
public class Main {
public static void main(String[] args) {
CircuitBreaker breaker = new CircuitBreaker(2, 3000);
for (int i = 0; i < 5; i++) {
try {
String result = breaker.call(() -> {
if (Math.random() < 0.7) throw new RuntimeException("Service error");
return "Data";
});
System.out.println("Call " + (i + 1) + ": " + result);
} catch (Exception e) {
System.out.println("Call " + (i + 1) + ": " + e.getMessage());
}
}
}
}
Explanation
Under the hood, the breaker cycles through three states:
- Closed: calls go through normally and failures are counted. A success resets the counter.
- Open: every call fails fast without touching the dependency — the caller gets a “circuit open” error in milliseconds instead of a 30 s timeout. What they see beyond that depends on your fallback: a cached/default response if you provided one, or an exception to handle. Calls stop reaching the service while a recovery timer runs.
- Half-open: when the recovery timeout elapses, a limited number of probe calls go through. Success closes the circuit; failure re-opens it and restarts the timer.
Before shipping any of these examples, two details deserve more attention than the diagram. First, what counts as a failure: a 404 or a validation error should usually not trip the breaker, while a timeout or connection reset should. The examples above count every exception for simplicity; a real breaker has to classify them. Second, concurrency: the Java version uses an AtomicInteger because several threads hit call() at once; a naive counter under counts under contention and the breaker trips late. The same applies in Node.js when calls run concurrently, though the event loop makes the counter itself safe.
The pattern works on any boundary where a call can stall or fail: HTTP clients, database drivers, file system calls. The Java example is synchronous (Supplier<T>); the JavaScript one is async because network calls there usually are.
Variants
| Variant | Behavior | Use Case |
|---|---|---|
| Count-based | Trip after N consecutive failures | Simple, predictable behavior |
| Time-based | Trip if the failure rate exceeds a threshold inside a sliding window | Adapts to varying load |
| Weighted | Different failure thresholds per exception type | Distinguish transient vs. permanent failures |
| Custom Fallback | Return a cached or default value when open instead of throwing | Graceful degradation (cache, default response) |
Production Libraries
Rolling your own is fine for learning, but production breakers also need metrics, sliding windows, and half-open permits. These are the standard options:
- Java: Resilience4j with
@CircuitBreakerannotations or the functional decorator API. - Python: pybreaker —
breaker = CircuitBreaker(fail_max=5, reset_timeout=60)wraps any callable. - JavaScript: opossum — a promise-based breaker where
fallback()is a first-class hook. - .NET: Polly’s
CircuitBreakerPolicy— the option most .NET teams end up using.
All four emit state-change events you can push into Prometheus or your APM, which is the main reason to prefer them over a hand-rolled version.
What Works
- Configure recovery timeouts based on the service’s typical restart time
- Log state transitions for observability and alerting
- Provide fallback behavior when the circuit is open (cached data, default response)
- Use separate circuit breakers for different downstream services
- Avoid sharing circuit breaker state across unrelated operations
Common Mistakes
- Setting the failure threshold too low, causing frequent false positives
- Setting the recovery timeout too short, not giving the service enough time to recover
- Using a single circuit breaker for all operations, causing unnecessary broad outages
- Not providing fallback behavior, leading to poor user experience when circuits are open
- Ignoring half-open state, never allowing recovery testing after a failure
Troubleshooting
- Breaker trips constantly during normal load: the failure threshold is too low, or business errors (4xx, validation exceptions) are being counted as failures. Classify exceptions so only infrastructure failures increment the counter.
- Breaker never recovers even though the service is healthy: the recovery timeout is longer than your incident duration, or every probe in half-open re-trips because the dependency is still warming up. Give probes their own timeout and consider two consecutive successes before closing.
- Fallback calls the same failing dependency: a fallback that hits the same downstream over the same connection pool defeats the purpose. Serve cached data or a static default, or route the fallback to a different cluster.
- Requests still time out while the breaker is open: fail fast means throwing immediately — if your wrapper still awaits the call, the breaker is only counting, not protecting. In practice that means checking the state before the client even builds the request.
- One slow dependency opens circuits for unrelated services: you shared a breaker (or its counter) across endpoints. Scope breakers per dependency, not per application.
See Also
- Companion code on GitHub — runnable breakers with exception classification and fallback in Python, JavaScript, and Java.
- Martin Fowler — CircuitBreaker — the original pattern write-up.
- Microsoft Azure — Circuit Breaker pattern — cloud implementation guidance.
- Resilience4j documentation — configuration reference for the Java implementation.
Frequently Asked Questions
How is circuit breaker different from retry?
Retry attempts the same operation multiple times and assumes the next attempt may succeed. The circuit breaker assumes repeated calls make things worse and stops calling altogether. They work together: retry inside the window for transient blips, breaker for persistent outages.
Should I use a library or implement my own?
For production, use a library — Resilience4j (Java), pybreaker (Python), opossum (Node), Polly (.NET). You get sliding-window metrics and half-open permits for free. Implement your own only to learn the mechanics or in very constrained environments. See also Ambassador for offloading resilience logic to a sidecar.
How do I monitor circuit breaker health?
Track three things: state transitions, failure rate, and time spent open. Alert when the breaker trips often or refuses to close. Resilience4j and opossum emit these events natively — with a hand-rolled breaker, logging every transition is the minimum.
Related Resources
Circuit Breaker with Monitoring
How to expose circuit breaker state as metrics for observability. Covers Prometheus integration, alerting rules, dashboards, and state transitions.
PatternRetry Pattern
Retry an operation that has failed with transient errors, using configurable strategies like fixed delay, exponential backoff, or circuit breaker integration.
PatternTimeout Pattern
Prevent operations from hanging indefinitely by enforcing a maximum execution time. A resilience pattern for predictable response times.
PatternGraceful Degradation Pattern
Degrade functionality instead of failing when dependencies are unavailable. Serve partial results, cached data, or fallback features to keep users running.
PatternBack-Pressure Pattern
Prevent upstream systems from overwhelming downstream consumers by propagating flow-control signals backward through the pipeline, ensuring stable throughput under load.
PatternSaga Pattern: Orchestration vs Choreography
Manage distributed transactions across multiple services by chaining local transactions with compensating actions for rollbacks. A microservices pattern.