RESILIENCE DESIGN · MICROSERVICES

Circuit Breaker Design Patterns in HTTP Clients

In distributed microservice architectures, cascading system failures are one of the most severe threats to overall availability. If a downstream microservice experiences high latency or database connection depletion, upstream services calling that endpoint will block waiting for HTTP socket timeouts.

As worker threads stack up waiting for unresponsive downstreams, the calling service exhausts its own thread pool and crashes—propagating a chain reaction of failures across the entire system. To prevent cascading failure storms, resilient systems implement the Circuit Breaker Design Pattern. In this technical article, we explore state machine mechanics, failure rate thresholds, exponential backoff with full jitter, and Python implementation.

1. The Three States of a Circuit Breaker

A circuit breaker acts as an intelligent proxy between microservices, maintaining an internal state machine with three distinct operational states:

Core Rule: Failing fast in the OPEN state prevents client applications from locking up threads while giving struggling downstream services time to recover without being overwhelmed by retry storms.

2. Production Python Circuit Breaker Implementation

Below is a thread-safe, asynchronous Python Circuit Breaker implementation wrapping the httpx HTTP client library:

import asyncio
import time
import httpx
from enum import Enum

class CircuitState(Enum):
    CLOSED = "CLOSED"
    OPEN = "OPEN"
    HALF_OPEN = "HALF_OPEN"

class CircuitBreakerOpenException(Exception):
    """Raised when request is blocked due to an open circuit breaker."""
    pass

class AsyncCircuitBreaker:
    def __init__(self, failure_threshold: int = 5, recovery_timeout: float = 15.0):
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.state = CircuitState.CLOSED
        self.failure_count = 0
        self.last_state_change = time.time()
        self._lock = asyncio.Lock()

    async def call(self, func, *args, **kwargs):
        async with self._lock:
            now = time.time()
            
            # Check if recovery timeout has elapsed to attempt trial request
            if self.state == CircuitState.OPEN:
                if now - self.last_state_change > self.recovery_timeout:
                    self.state = CircuitState.HALF_OPEN
                    self.last_state_change = now
                else:
                    raise CircuitBreakerOpenException("Circuit is OPEN. Downstream service unavailable.")

        try:
            # Execute actual HTTP request
            result = await func(*args, **kwargs)
            
            async with self._lock:
                if self.state == CircuitState.HALF_OPEN:
                    # Successful trial request resets circuit to CLOSED
                    self.state = CircuitState.CLOSED
                    self.failure_count = 0
                    self.last_state_change = time.time()
            return result

        except Exception as err:
            async with self._lock:
                self.failure_count += 1
                if self.failure_count >= self.failure_threshold or self.state == CircuitState.HALF_OPEN:
                    self.state = CircuitState.OPEN
                    self.last_state_change = time.time()
            raise err

# Usage Example with httpx AsyncClient
circuit_breaker = AsyncCircuitBreaker(failure_threshold=3, recovery_timeout=10.0)

async def fetch_user_profile(user_id: int):
    async with httpx.AsyncClient(timeout=2.0) as client:
        return await client.get(f"https://api.riddletech.co.ke/users/{user_id}")

async def safe_api_call(user_id: int):
    try:
        response = await circuit_breaker.call(fetch_user_profile, user_id)
        return response.json()
    except CircuitBreakerOpenException:
        # Fallback response when downstream is offline
        return {"status": "degraded", "user_id": user_id, "cached": True}
    except Exception as err:
        return {"status": "error", "message": str(err)}

3. Exponential Backoff with Full Jitter

When combined with retries, fixed retry delays cause thundering herd problems where thousands of clients retry at the exact same millisecond mark. AWS research recommends Exponential Backoff with Full Jitter:

import random

def calculate_backoff_with_jitter(attempt: int, base_delay: float = 1.0, max_delay: float = 30.0) -> float:
    """Calculates exponential backoff with full randomized jitter."""
    temp = min(max_delay, base_delay * (2 ** attempt))
    sleep_duration = random.uniform(0, temp)
    return sleep_duration

Conclusion

Implementing the Circuit Breaker pattern with exponential backoff and fallback defaults builds resilient microservice networks. By insulating downstream services from retry storms during outages, systems maintain stability under peak load.