Skip to content

Circuit BreakerΒΆ

Cello includes a circuit breaker middleware implemented in Rust. It monitors response failures and temporarily stops processing requests when a failure threshold is exceeded, giving downstream services time to recover.

Quick StartΒΆ

from cello import App

app = App()

app.enable_circuit_breaker(
    failure_threshold=5,     # Open after 5 failures
    reset_timeout=30,        # Wait 30 seconds before half-open
    half_open_target=3       # 3 successes to close again
)

The Circuit Breaker PatternΒΆ

The circuit breaker operates in three states:

                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                β”‚       CLOSED         β”‚
                β”‚  (normal operation)  β”‚
                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
              failure_threshold reached
                           β”‚
                           β–Ό
                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                β”‚        OPEN          β”‚
                β”‚ (reject all requests)β”‚
                β”‚  Returns 503        β”‚
                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                  reset_timeout expires
                           β”‚
                           β–Ό
                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                β”‚     HALF-OPEN        β”‚
                β”‚ (allow test requests)β”‚
                β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
                      β”‚          β”‚
           success    β”‚          β”‚  failure
           count met  β”‚          β”‚
                      β–Ό          β–Ό
                   CLOSED      OPEN

StatesΒΆ

State Behavior
Closed Normal operation. Requests pass through. Failures are counted.
Open All requests are rejected immediately with 503 Service Unavailable. No load on downstream services.
Half-Open A limited number of test requests are allowed through. If they succeed, the circuit closes. If they fail, it opens again.

ConfigurationΒΆ

app.enable_circuit_breaker(
    failure_threshold=5,
    reset_timeout=30,
    half_open_target=3,
    failure_codes=[500, 502, 503, 504]
)
Parameter Type Default Description
failure_threshold int 5 Number of failures before opening the circuit
reset_timeout int 30 Seconds to wait in Open state before moving to Half-Open
half_open_target int 3 Successful requests needed in Half-Open to close the circuit
failure_codes list[int] [500, 502, 503, 504] HTTP status codes considered failures

How Failures Are DetectedΒΆ

The circuit breaker counts responses with status codes in the failure_codes list. By default, these are server errors:

  • 500 Internal Server Error
  • 502 Bad Gateway
  • 503 Service Unavailable
  • 504 Gateway Timeout

Client errors (4xx) are not counted as failures because they indicate bad input, not a system problem.

Custom Failure CodesΒΆ

# Include 408 Request Timeout as a failure
app.enable_circuit_breaker(
    failure_threshold=5,
    reset_timeout=30,
    failure_codes=[408, 500, 502, 503, 504]
)

Response When OpenΒΆ

When the circuit is open, all requests receive:

HTTP/1.1 503 Service Unavailable
Content-Type: application/json
Retry-After: 25

{"error": "Service temporarily unavailable", "retry_after": 25}

The Retry-After header tells clients how many seconds until the circuit enters Half-Open and may start accepting requests again.


Example: Protecting a Fragile EndpointΒΆ

from cello import App

app = App()

# Circuit breaker protects all routes
app.enable_circuit_breaker(
    failure_threshold=3,      # Open after just 3 failures
    reset_timeout=60,         # Wait 1 minute before retrying
    half_open_target=2        # 2 successes to fully recover
)

@app.get("/api/external-data")
async def external_data(request):
    # If this endpoint fails 3 times in a row,
    # the circuit opens and returns 503 for 60 seconds
    data = await fetch_from_external_api()
    return {"data": data}

@app.get("/health")
def health(request):
    return {"status": "ok"}

Recovery FlowΒΆ

A typical failure and recovery sequence:

Time 0s   - Request 1: 500 (failure count: 1)
Time 1s   - Request 2: 500 (failure count: 2)
Time 2s   - Request 3: 500 (failure count: 3)
Time 3s   - Request 4: 500 (failure count: 4)
Time 4s   - Request 5: 500 (failure count: 5 β†’ CIRCUIT OPENS)
Time 5s   - Request 6: 503 (circuit open, rejected)
Time 10s  - Request 7: 503 (circuit open, rejected)
Time 34s  - Circuit enters HALF-OPEN
Time 35s  - Request 8: 200 (success count: 1)
Time 36s  - Request 9: 200 (success count: 2)
Time 37s  - Request 10: 200 (success count: 3 β†’ CIRCUIT CLOSES)
Time 38s  - Request 11: 200 (normal operation)

Combining with Other MiddlewareΒΆ

The circuit breaker works well alongside rate limiting and caching:

app = App()

# Rate limiting prevents abuse
app.enable_rate_limit(RateLimitConfig.token_bucket(requests=100, window=60))

# Circuit breaker protects against cascading failures
app.enable_circuit_breaker(failure_threshold=5, reset_timeout=30)

# Caching reduces load on recovering services
app.enable_caching(ttl=60)

Defense in Depth

Rate limiting protects your server from external abuse. The circuit breaker protects your server from downstream failures. Together they provide comprehensive fault tolerance.


PerformanceΒΆ

The circuit breaker uses atomic state checks in Rust:

Operation Overhead
State check (Closed) ~50ns
State check (Open, reject) ~20ns
Failure counter update ~50ns

When the circuit is open, requests are rejected in under 20 nanoseconds -- faster than any other middleware because no processing occurs at all.


Next StepsΒΆ