StackPractices
intermediate By Mathias Paulenko

Circuit Breaker Pattern in Python, JavaScript, and Java

Prevent cascading failures by stopping requests to failing services. An architectural pattern for resilient distributed systems.

Topics: design

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:

flowchart diagram: CLOSED[
  • 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

VariantBehaviorUse Case
Count-basedTrip after N consecutive failuresSimple, predictable behavior
Time-basedTrip if the failure rate exceeds a threshold inside a sliding windowAdapts to varying load
WeightedDifferent failure thresholds per exception typeDistinguish transient vs. permanent failures
Custom FallbackReturn a cached or default value when open instead of throwingGraceful 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 @CircuitBreaker annotations 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

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.