Skip to content

Circuit Breaker Pattern at the API Gateway

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you'll learn about Circuit Breaker at Gateway. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

The Circuit Breaker Pattern prevents repeated requests to a failing backend service, giving it time to recover and preventing cascading failures across the system.

What You'll Learn

By the end of this lesson, you will implement circuit breaker with three states (closed, open, half-open), configure thresholds, and design fallback responses.

Why It Matters

Without circuit breakers, a failing service causes other services to wait for responses, leading to thread exhaustion and system-wide failure (cascading).

Real-World Use

When the payment service starts failing, the circuit breaker trips after 5 failures. Subsequent requests receive a cached "service unavailable" response instead of timing out.

Circuit Breaker States

flowchart TD
    Closed[CLOSED - Normal] -->|Failure threshold reached| Open[OPEN - Failing]
    Open -->|Timeout elapsed| HalfOpen[HALF-OPEN - Testing]
    HalfOpen -->|Test request succeeds| Closed
    HalfOpen -->|Test request fails| Open

Circuit Breaker Implementation

# circuit_breaker.py
import time
from typing import Any, Callable, Optional

class CircuitBreakerState:
    CLOSED = "CLOSED"
    OPEN = "OPEN"
    HALF_OPEN = "HALF_OPEN"

class CircuitBreaker:
    def __init__(self, failure_threshold: int = 5,
                 recovery_timeout: float = 30.0,
                 half_open_max_requests: int = 3):
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.half_open_max_requests = half_open_max_requests

        self.state = CircuitBreakerState.CLOSED
        self.failure_count = 0
        self.last_failure_time = 0.0
        self.half_open_requests = 0

    def call(self, func: Callable, *args, **kwargs) -> Any:
        if self.state == CircuitBreakerState.OPEN:
            if time.time() - self.last_failure_time >= self.recovery_timeout:
                self.state = CircuitBreakerState.HALF_OPEN
                self.half_open_requests = 0
            else:
                raise Exception("Circuit breaker is OPEN. Service unavailable.")

        if self.state == CircuitBreakerState.HALF_OPEN:
            if self.half_open_requests >= self.half_open_max_requests:
                raise Exception("Circuit breaker is HALF_OPEN. Too many test requests.")

        try:
            result = func(*args, **kwargs)
            self._on_success()
            return result
        except Exception as e:
            self._on_failure()
            raise

    def _on_success(self):
        if self.state == CircuitBreakerState.HALF_OPEN:
            self.state = CircuitBreakerState.CLOSED
            self.failure_count = 0

    def _on_failure(self):
        self.failure_count += 1
        self.last_failure_time = time.time()

        if self.state == CircuitBreakerState.HALF_OPEN:
            self.state = CircuitBreakerState.OPEN
        elif self.failure_count >= self.failure_threshold:
            self.state = CircuitBreakerState.OPEN

    def get_state(self) -> str:
        return self.state

cb = CircuitBreaker(failure_threshold=3, recovery_timeout=2)

def failing_service():
    raise ConnectionError("Backend service unavailable")

def working_service():
    return {"status": "ok"}

results = []
for i in range(5):
    try:
        result = cb.call(failing_service)
        results.append("success")
    except Exception as e:
        results.append(str(e))

print("State after failures:", cb.get_state())

time.sleep(3)
try:
    result = cb.call(failing_service)
except Exception as e:
    print("After recovery timeout:", str(e))

print("State:", cb.get_state())

Expected output:

State after failures: OPEN
After recovery timeout: Circuit breaker is OPEN. Service unavailable.
State: OPEN

Circuit Breaker with Fallback

# circuit_breaker_fallback.py
import time
from typing import Any, Callable, Dict, Optional

class CircuitBreakerWithFallback:
    def __init__(self, name: str, failure_threshold: int = 5,
                 recovery_timeout: float = 30.0):
        self.name = name
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.state = "CLOSED"
        self.failure_count = 0
        self.last_failure_time = 0.0
        self.fallback_cache: Optional[Dict] = None

    def call(self, func: Callable, fallback: Optional[Callable] = None,
             *args, **kwargs) -> Any:
        if self.state == "OPEN":
            if time.time() - self.last_failure_time >= self.recovery_timeout:
                self.state = "HALF_OPEN"
            elif fallback:
                return self._execute_fallback(fallback)
            else:
                raise Exception(f"Circuit breaker {self.name} is OPEN")

        try:
            result = func(*args, **kwargs)
            self._on_success()
            return result
        except Exception as e:
            self._on_failure()
            if fallback:
                return self._execute_fallback(fallback)
            raise

    def _execute_fallback(self, fallback: Callable) -> Any:
        result = fallback()
        self.fallback_cache = result
        return result

    def _on_success(self):
        if self.state == "HALF_OPEN":
            print(f"  {self.name}: Half-open test succeeded, closing circuit")
        self.state = "CLOSED"
        self.failure_count = 0

    def _on_failure(self):
        self.failure_count += 1
        self.last_failure_time = time.time()
        if self.state == "HALF_OPEN":
            self.state = "OPEN"
            print(f"  {self.name}: Half-open test failed, reopening circuit")
        elif self.failure_count >= self.failure_threshold:
            self.state = "OPEN"

cb = CircuitBreakerWithFallback("payment-service", failure_threshold=3, recovery_timeout=5)

def failing():
    raise Exception("Service down")

def cached_response():
    return {"data": "cached", "source": "fallback"}

for i in range(4):
    try:
        result = cb.call(failing, fallback=cached_response)
        print(f"Request {i+1}: {result}")
    except Exception as e:
        print(f"Request {i+1}: {e}")
    print(f"  State: {cb.state}")

Expected output:

Request 1: {'data': 'cached', 'source': 'fallback'}
  State: CLOSED
Request 2: {'data': 'cached', 'source': 'fallback'}
  State: CLOSED
Request 3: {'data': 'cached', 'source': 'fallback'}
  State: OPEN
Request 4: {'data': 'cached', 'source': 'fallback'}
  State: OPEN

Common Mistakes

1. No Fallback Responses

An open circuit breaker should return a fallback, not an error. Clients prefer stale data over no data.

2. Too Short Recovery Timeout

If the recovery timeout is too short, the circuit toggles rapidly. Set it based on the service's typical recovery time.

3. Counting All Errors

Not all errors signal a downed service. Count 5xx errors and timeouts, not 4xx client errors.

4. No Per-Service Circuit Breakers

A single circuit breaker for all services means one failing service blocks all traffic. Use per-service breakers.

5. Not Logging State Changes

Circuit breaker state changes are critical events. Log every state transition with timestamps and failure counts.

Practice Questions

1. What are the three states of a circuit breaker?

CLOSED (normal operation), OPEN (failing, requests blocked), HALF-OPEN (testing if service recovered).

2. How does the circuit breaker prevent cascading failures?

By failing fast instead of waiting for timeouts, it prevents thread and Connection Pool exhaustion from spreading.

3. What happens in the half-open state?

The circuit breaker allows a limited number of test requests. If they succeed, the circuit closes. If they fail, it reopens.

4. Why use fallback responses?

Clients receive a degraded response instead of an error. Fallbacks improve user experience during partial outages.

Challenge

Design a circuit breaker system for a microservice architecture with per-service breakers, slamming (rapid open after first failure), exponential backoff recovery, and Redis-backed state sharing.

FAQ

Should the gateway or service implement circuit breakers?

Both. Gateway breakers protect the entry point. Service breakers protect inter-service calls.

What is the difference between circuit breaker and retry?

Retry repeats failed requests. Circuit breaker stops requests entirely to give the service time to recover.

How do you choose failure thresholds?

Start with 5 failures in 60 seconds. Adjust based on service criticality and typical error rates.

Can circuit breakers cause cascading failures?

Badly configured breakers can cause premature tripping. Monitor and tune thresholds based on real traffic patterns.

What metrics should I track for circuit breakers?

State transitions, failure counts, request counts while open, fallback invocation count, and half-open test results.

Mini Project: Circuit Breaker Manager

# cb_manager.py
import time
from typing import Any, Callable, Dict, Optional

class CircuitBreakerManager:
    def __init__(self):
        self.breakers: Dict[str, dict] = {}

    def register(self, service: str, threshold: int = 5, timeout: float = 30):
        self.breakers[service] = {
            "state": "CLOSED", "failures": 0, "threshold": threshold,
            "timeout": timeout, "last_fail": 0,
        }

    def call(self, service: str, func: Callable, fallback: Optional[Callable] = None) -> Any:
        cb = self.breakers.get(service)
        if not cb:
            return func()

        if cb["state"] == "OPEN":
            if time.time() - cb["last_fail"] >= cb["timeout"]:
                cb["state"] = "HALF_OPEN"
            elif fallback:
                return fallback()
            else:
                raise Exception(f"{service} circuit is OPEN")

        try:
            result = func()
            cb["state"] = "CLOSED"
            cb["failures"] = 0
            return result
        except Exception:
            cb["failures"] += 1
            cb["last_fail"] = time.time()
            if cb["failures"] >= cb["threshold"] or cb["state"] == "HALF_OPEN":
                cb["state"] = "OPEN"
            if fallback:
                return fallback()
            raise

mgr = CircuitBreakerManager()
mgr.register("payments", threshold=2, timeout=3)

def fail():
    raise Exception("fail")

def fallback():
    return "cached"

mgr.call("payments", fail, fallback)
mgr.call("payments", fail, fallback)
result = mgr.call("payments", fail, fallback)
print(f"Result when open: {result}")
print(f"State: {mgr.breakers['payments']['state']}")

Expected output:

Result when open: cached
State: OPEN

What's Next

You understand the circuit breaker pattern. Next, learn about gateway caching, then explore request aggregation.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro