28.6. Circuit Breakers
Table of Contents
Why Circuit Breakers Matter
In a production backend, failures are normal. Services can become slow, databases can overload, networks can be flaky. If your code keeps calling a failing dependency again and again, you waste resources and often make the situation worse.
A circuit breaker is a defensive pattern that stops your system from repeatedly calling something that is already known to be failing. It protects:
- Your own service resources (threads, connections, CPU)
- Downstream services from overload during an incident
- User experience, by failing fast with clear errors
You can think of it as an automatic switch that cuts off a bad electrical circuit to prevent a fire. Here it cuts off bad remote calls to prevent cascading failures.
Key idea: A circuit breaker monitors calls to a dependency and opens when failures pass a threshold. While open, it blocks calls immediately, instead of trying and failing repeatedly.
Basic Circuit Breaker States
A circuit breaker has three main states that describe whether calls are allowed through.
Closed
In the Closed state, everything is normal.
- All calls to the dependency are allowed.
- The circuit breaker tracks:
- Number of recent calls
- Number of recent failures
- Response times (in some implementations)
If the failure rate crosses a configured threshold, the breaker opens.
Example configuration:
| Setting | Example value |
|---|---|
| Failure rate threshold | 50% failures |
| Minimum calls to assess | 20 calls |
| Sliding window | Last 30 seconds |
If, within the last 30 seconds, at least 20 calls were made and 10 or more failed, the breaker moves from Closed to Open.
Open
In the Open state:
- The dependency is considered unhealthy.
- No calls are forwarded to the dependency.
- Instead, the breaker:
- Fails fast with an error
- Or returns a fallback response
This prevents:
- Wasting time on slow failing requests
- Overloading a service that is already in trouble
The breaker stays open for a configured cooldown period, for example 30 seconds. After that, it moves to Half-Open to test if the dependency has recovered.
Half-Open
In the Half-Open state:
- Only a limited number of test calls are allowed to pass through.
- All other calls are still rejected or get a fallback.
- The breaker observes the results of these test calls.
Two outcomes are possible:
- Enough test calls succeed
The dependency looks healthy again.
The breaker moves to Closed, resets failure counters, and allows all calls again. - Enough test calls fail
The dependency is still unhealthy.
The breaker moves back to Open and starts a new cooldown period.
Table summary:
| State | Calls allowed? | When entered |
|---|---|---|
| Closed | All calls, until failures spike | On startup or after Half-Open success |
| Open | No calls (only fast fail/fallback) | When failure threshold is exceeded in Closed |
| Half-Open | Few test calls allowed | After Open timeout, to check if service recovered |
What Circuit Breakers Protect Against
Circuit breakers are helpful in several common failure scenarios.
Slow or Hung Dependencies
Imagine your service calls a payment gateway that becomes very slow. Without a circuit breaker:
- Every request to your API waits on the slow gateway.
- Request threads pile up.
- Your service becomes slow or crashes.
With a circuit breaker:
- Once enough calls time out, the breaker opens.
- Future requests fail quickly or use a fallback.
- Your own resources are protected, even if the gateway is still slow.
Cascading Failures
In a microservices system, one failing service can trigger failures in many others.
Without circuit breakers:
- Service A calls B.
- B calls C.
- C is down.
- B keeps calling C, gets stuck, then A gets stuck, then the frontend fails.
With circuit breakers between A and B, and between B and C:
- B opens its breaker to C when C fails.
- B may return partial or cached data to A.
- A may open its breaker to B if needed.
- The blast radius is contained.
Thundering Herd During Recovery
When a service starts to recover, many clients might retry at once and overload it again.
A Half-Open state helps here:
- Only a few calls are allowed through at first.
- If they succeed, traffic slowly returns.
- If they fail, the breaker opens again and the service gets more time.
Key Configuration Parameters
You control the behavior of a circuit breaker by setting thresholds and timing values. Choosing good values is important.
Failure Thresholds
Two common types:
- Failure count threshold
Open the breaker after a fixed number of failures in a row.
Example:
- Open after 5 consecutive failures.
- If even 1 call succeeds, the count resets.
- Failure rate threshold
Open the breaker when the ratio of failures to total calls is too high.
Example:
- Minimum 20 calls in the sliding window.
- If at least 50% are failures, open the breaker.
Typical rule:
Open the circuit when the failure rate exceeds a threshold (for example 50%) over a minimum number of recent calls (for example 20), within a sliding time window (for example 30 seconds).
Do not use very small windows, or your breaker might open and close too often because of random noise.
Timeouts and Durations
You usually configure:
- Call timeout
How long to wait for a dependency before treating it as a failure.
For example, 1 second for a normal API call, 5 seconds for a rare heavy task. - Open state duration
How long the breaker stays Open before moving to Half-Open.
For example, 30 seconds or 1 minute. - Half-Open test period
Not always a time value, but a number of test calls.
For example, allow 10 test calls, then decide.
If you pick a very short call timeout, you may mark a healthy but slow service as failing. If you pick a very long Open duration, recovery will be slow even when the dependency is fixed.
Allowed Calls in Half-Open
In Half-Open, you usually configure:
- Max test calls: for example 10.
- Success threshold: for example 8 of 10 must succeed.
- Failure threshold: for example 3 failures immediately re-open.
Example rule:
- Half-Open allows up to 10 test calls.
- If 8 succeed before 3 fail, close the breaker.
- If 3 fail before 8 succeed, open the breaker again.
Implementing Circuit Breakers in Practice
You usually do not write a circuit breaker from scratch in production. Most languages have libraries that implement the pattern.
In Python, the simplest approach is a small custom implementation plus a decorator.
A Simple Python Circuit Breaker
This is a minimal example for education, not production ready. It shows the structure clearly.
import time
from functools import wraps
class CircuitOpenError(Exception):
pass
class CircuitBreaker:
def __init__(
self,
failure_threshold=5,
recovery_timeout=30,
expected_exception=Exception,
):
self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout
self.expected_exception = expected_exception
self.state = "closed" # "closed", "open", "half_open"
self.failure_count = 0
self.last_failure_time = None
def _open(self):
self.state = "open"
self.last_failure_time = time.time()
self.failure_count = 0
def _half_open(self):
self.state = "half_open"
self.failure_count = 0
def _close(self):
self.state = "closed"
self.failure_count = 0
self.last_failure_time = None
def _can_attempt(self):
if self.state == "closed":
return True
if self.state == "open":
# Check if cooldown passed
if (time.time() - self.last_failure_time) >= self.recovery_timeout:
self._half_open()
return True
return False
if self.state == "half_open":
# Allow a single trial call at a time for simplicity
return True
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
if not self._can_attempt():
raise CircuitOpenError("Circuit is open. Failing fast.")
try:
result = func(*args, **kwargs)
except self.expected_exception:
self.failure_count += 1
if self.state == "half_open":
# Any failure in half-open re-opens
self._open()
elif self.failure_count >= self.failure_threshold:
self._open()
raise
else:
# Success
if self.state in ("half_open", "open"):
self._close()
else:
# Closed and successful, keep counters clean
self.failure_count = 0
return result
return wrapperUsage example:
import requests
breaker = CircuitBreaker(failure_threshold=3, recovery_timeout=10)
@breaker
def call_payment_service(order_id: str):
response = requests.post(
"https://payments.example.com/pay",
json={"order_id": order_id},
timeout=1.0, # Network timeout, separate from breaker logic
)
response.raise_for_status()
return response.json()
def pay_for_order(order_id: str):
try:
result = call_payment_service(order_id)
return {"status": "ok", "payment_id": result["id"]}
except CircuitOpenError:
# Fallback: queue payment for later processing
return {"status": "queued", "reason": "payment service unavailable"}
except requests.RequestException:
# Real failure
return {"status": "failed", "reason": "payment request error"}Flow:
- If
call_payment_servicefails 3 times in a row, the breaker opens. - While open, any call to
call_payment_serviceraisesCircuitOpenErrorimmediately. - After 10 seconds of being open, the breaker becomes Half-Open and allows a trial call.
- If that trial call succeeds, the breaker closes and normal traffic resumes.
Integrating with FastAPI
In FastAPI, you usually wrap client calls, not endpoints themselves. Example:
from fastapi import FastAPI, HTTPException
import httpx
app = FastAPI()
weather_breaker = CircuitBreaker(failure_threshold=5, recovery_timeout=30)
@weather_breaker
def fetch_weather(city: str):
with httpx.Client(timeout=1.0) as client:
resp = client.get("https://weather.example.com/current", params={"city": city})
resp.raise_for_status()
return resp.json()
@app.get("/weather/{city}")
async def get_weather(city: str):
try:
data = fetch_weather(city)
return {"city": city, "temperature": data["temp"]}
except CircuitOpenError:
# Return a clear but graceful error
raise HTTPException(
status_code=503,
detail="Weather service temporarily unavailable. Please try again later.",
)
except httpx.HTTPError:
# Other network or HTTP errors
raise HTTPException(
status_code=502,
detail="Failed to fetch data from weather provider.",
)Here:
- The circuit breaker sits around
fetch_weather. - The API endpoint handles
CircuitOpenErrorwith a 503 status, which is appropriate for a temporary outage.
Fallback Strategies
When a circuit breaker opens, you have a choice: just error, or provide a fallback.
Common strategies:
| Strategy | Description | Example |
|---|---|---|
| Fail fast | Return an error immediately | 503 Service Unavailable |
| Cached response | Return last known good data | Cached product info |
| Degraded functionality | Provide a simpler version of the feature | Show prices but disable live discounts |
| Queue for later | Put work into a background queue for retry | Queue payments or emails |
| Alternative provider | Call another similar service if one is down | Use backup SMS or email provider |
Example with a cache fallback:
from fastapi import FastAPI, HTTPException
from typing import Dict
app = FastAPI()
product_cache: Dict[str, dict] = {}
product_breaker = CircuitBreaker(failure_threshold=3, recovery_timeout=20)
@product_breaker
def fetch_product_from_service(product_id: str) -> dict:
# Imagine this calls a remote microservice
...
@app.get("/products/{product_id}")
def get_product(product_id: str):
try:
product = fetch_product_from_service(product_id)
product_cache[product_id] = product # Update cache
return product
except CircuitOpenError:
# Try to serve from cache
cached = product_cache.get(product_id)
if cached:
return {**cached, "warning": "Serving cached data"}
raise HTTPException(
status_code=503,
detail="Product service unavailable and no cached data.",
)This provides a better user experience during outages, especially for mostly-read data like products or articles.
Circuit Breakers vs Retries
Circuit breakers and retries are often used together, but they serve different purposes and can conflict if used carelessly.
What Retries Do
A retry is when you try the same failing request again, usually with some delay. This can hide temporary network glitches.
Examples:
- Retry once after 100 ms.
- Retry up to 3 times, with exponential backoff:
- Wait 100 ms, then 200 ms, then 400 ms.
Dangerous Combination
If you blindly combine retries with circuit breakers:
- Each user request might trigger multiple retry attempts.
- Each retry attempt passes through the circuit breaker.
- Failures accumulate quickly and the breaker opens often.
- Your system does even more work during an outage.
To use both safely:
- Use small retry counts and timeouts.
- Apply retries on idempotent operations only.
- Apply the circuit breaker around the retried call, not inside each small retry attempt, or the opposite, but be deliberate.
- Implement exponential backoff to avoid bursts of calls.
Important rule:
Never use unlimited or aggressive retries with circuit breakers. Retries should reduce noise from brief glitches, not hammer a failing service.
Where to Place Circuit Breakers
In a real system, you might have many dependencies.
Typical places to use circuit breakers:
- HTTP clients to other microservices
- Database-like external services that you cannot control
- External APIs like payment providers, SMS services, email services
- Any dependency that:
- Is used frequently
- Has non-trivial latency
- Can fail independently of your service
You usually do not put a breaker around:
- Local in-process code
- Your own database that you control directly
For a database, connection pools, timeouts, and proper error handling are usually more important.
Instead of sprinkling circuit breaker code everywhere, it is better to:
- Wrap your HTTP client in a small helper module that applies timeout, retry, and circuit breaker in one place.
- Make all service-to-service calls go through that helper.
Metrics, Logging, and Observability
Circuit breakers change how your system behaves under failure, so you must observe them.
Useful Metrics
Track at least:
- Number of calls passed through the breaker
- Number of failures
- Current state (Closed, Open, Half-Open)
- Number of times the breaker opened
- Time spent in each state
If you use Prometheus, you might have metrics like:
circuit_breaker_state{breaker="payment"}
0 = Closed, 1 = Open, 2 = Half-Opencircuit_breaker_open_total{breaker="payment"}
Plot these in Grafana to see patterns like:
- A breaker that is almost always in Open state
This suggests a permanently failing dependency. - A breaker that flaps often between Open and Closed
This suggests thresholds that are too sensitive.
Logging
Log every state change:
- When the breaker opens, log:
- Which dependency
- Failure rate or count
- Error examples
- When it transitions to Half-Open
- When it closes after recovery
These logs are very helpful during incident analysis, because they show when your system started protecting itself and when it recovered.
Common Mistakes and Pitfalls
Several problems appear often with circuit breakers.
Using One Global Breaker for Everything
If you use a single breaker for calls to many different services, a problem in one service will block all others.
Better:
- Use one breaker per dependency, for example:
payment_breakeremail_breakersearch_breaker
Too Aggressive or Too Lazy Thresholds
- If thresholds are too aggressive:
- The breaker opens for small spikes of failures.
- Users see errors even when the dependency is mostly fine.
- If thresholds are too lazy:
- The breaker opens late.
- Your service spends a lot of time stuck on failing calls.
You will typically need to adjust these values using real traffic data, not only guesses.
Ignoring Timeouts
A circuit breaker cannot fix calls that never time out. Always pair it with proper network timeouts and database timeouts.
Example:
# Bad: no timeout, requests might hang for a long time
requests.get("https://service", params={"id": 1})
# Better: explicit timeout
requests.get("https://service", params={"id": 1}, timeout=1.0)If you forget timeouts, failures may not be counted correctly, and your breaker logic will not trigger when it should.
Forgetting Idempotency
If you combine retries with circuit breakers, you will call the same dependency multiple times. For non-idempotent operations, this is risky.
Examples of non-idempotent operations:
- "Charge credit card"
- "Send email"
In such cases:
- Make upstream operations idempotent if possible.
For example, use idempotency keys for payments. - Or use careful retry logic at the business level, not blindly around HTTP calls.
Summary
Circuit breakers are a central part of building resilient backends in production:
- They protect your service and downstream dependencies during failure.
- They have three states, Closed, Open, and Half-Open, to manage how calls are allowed.
- They depend on good configuration: thresholds, timeouts, and detection windows.
- They must work together with retries, timeouts, and fallbacks, not fight them.
- They are especially important in microservices architectures and when calling external providers.
In a production backend, you should:
- Use a well-tested library for circuit breakers.
- Wrap your outbound calls through a small, reusable client helper.
- Expose metrics and logs that show circuit breaker behavior.
- Test failure scenarios in staging so that you know how your system behaves when dependencies go bad.
Views: 7
KAHIBARO