Circuit Breaker Pattern at the API Gateway
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
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