INTRODUCTION: THE CASCADING FAILURE NIGHTMARE
In Lessons 7.1 and 7.2, we built the API Gateway and the rate limiter. We tuned NGINX worker processes, enabled HTTP/2, implemented distributed token buckets with Redis, and designed the 429 Too Many Requests response with jittered retry. However, the ASPSP’s backend is a distributed system of microservices. The Payment Service depends on the Consent Service, the Balance Service, the Fraud Engine, and the Clearing House (an external dependency). If the Clearing House becomes slow (e.g., due to a spike in traffic), the Payment Service’s threads block, queuing up requests. Eventually, the Payment Service exhausts its thread pool, causing timeouts for all incoming requests—a cascading failure.
A cascading failure is the single most destructive operational event in a distributed system. It starts with a single slow dependency. The dependency’s latency increases from 50ms to 5 seconds. The Payment Service’s thread pool (size 100) becomes saturated. New requests queue up, waiting for threads. The queue grows indefinitely. Eventually, the Payment Service runs out of memory (OOM) and crashes. The API Gateway, unable to connect to the Payment Service, returns 502 Bad Gateway errors. The TPPs retry aggressively, amplifying the load. The entire system collapses.
The Circuit Breaker pattern prevents cascading failures. It monitors the success/failure rate of calls to a downstream service. If the failure rate exceeds a threshold (e.g., 50% over a 60-second sliding window), the circuit breaker opens. Subsequent calls to the downstream service are immediately rejected (without attempting the call), returning a fallback response (e.g., a cached value or a graceful error). After a timeout (e.g., 30 seconds), the circuit breaker transitions to half-open, allowing a limited number of test requests to check if the service has recovered. If the test succeeds, the circuit closes; if it fails, the circuit re-opens.
This lesson formalizes the circuit breaker and retry patterns at a level of depth commensurate with a senior quantitative architect. We define the three states (Closed, Open, Half-Open) and the transition logic. We derive the optimal failure threshold using the Beta distribution (P(Failure) = (failures + 1) / (total + 2)), which provides a Bayesian estimate of the true failure rate. We implement the retry with exponential backoff and jitter algorithm, as defined in Lesson 2.3, but extended to the API Gateway layer. We quantify the latency impact of a circuit breaker (which is 0ms in the Closed state, and 2ms for the Half-Open check). We also derive the optimal timeout values for downstream dependencies using the tail latency analysis (p99.9 latency), and we prove that the circuit breaker reduces the probability of a cascading failure from 20% to < 0.01%.
LEARNING OBJECTIVES
-
Define the Circuit Breaker State Machine—formalizing the three states:
Closed(normal operation),Open(circuit broken, requests rejected), andHalf-Open(testing recovery), and deriving the transition logic based on the failure rate and theopen_timeoutparameter. -
Derive the Optimal Failure Threshold—using the Beta distribution to estimate the probability of failure, and setting the failure threshold to 50% (the industry standard). We will prove that a threshold of 50% maximises the F1-score (Precision vs. Recall) for failure detection.
-
Implement the Retry with Exponential Backoff and Jitter—defining the algorithm
delay = min(max_delay, base_delay * 2^attempt) + random(0, jitter), and proving that jitter reduces thundering herds by 80%. We will also derive the optimalbase_delay(100ms) andmax_delay(30s) based on the ASPSP’s recovery time. -
Quantify the Circuit Breaker Latency—measuring the overhead of the circuit breaker check (0.05ms in-memory), and the Half-Open test request (50ms), and proving that the circuit breaker adds 0ms latency in the common case (Closed state).
-
Derive the Optimal Timeout Values—using tail latency analysis (p99.9 latency) to set the timeout for each downstream dependency. For the Payment Service, if the p99.9 latency is 150ms, we set the timeout to 300ms (2× the p99.9). We prove that this timeout catches 99.99% of requests, preventing long-tail latency from causing cascading failures.
-
Model the Cascading Failure Probability—using a Markov chain model to calculate the probability of a cascading failure with and without the circuit breaker, and proving that the circuit breaker reduces the probability from 20% to < 0.01%.
-
Design the Fallback Response Strategy—defining the fallback responses for each downstream service (e.g., returning cached data for balance queries, returning a “Payment Pending” status for payment submissions), and proving that the fallback response is acceptable to the TPP.
PART 1: THE CIRCUIT BREAKER STATE MACHINE — The Formal Definition
The circuit breaker has three states: Closed, Open, and Half-Open. The state transitions are governed by a failure rate metric, computed over a sliding window.
+-----------------------------------------------------------------------+ | CIRCUIT BREAKER STATE MACHINE | +-----------------------------------------------------------------------+ | | | CLOSED (Normal Operation) | | +------------------------------------------------------------------+ | | | • Requests pass through to the downstream service. | | | | • Failure count increments on errors (timeouts, 5xx). | | | | • Success count increments on successful requests. | | | | • Failure rate = failures / (failures + successes). | | | | • If failure rate > 50% over a 60-second window: | | | | → Transition to OPEN. | | | +------------------------------------------------------------------+ | | | | | | (Failure rate > 50% over 60s) | | v | | OPEN (Circuit Broken) | | +------------------------------------------------------------------+ | | | • Requests are rejected immediately (fallback response). | | | | • Timer starts (open_timeout = 30s). | | | | • No requests are sent to the downstream service. | | | | • After 30s: → Transition to HALF-OPEN. | | | +------------------------------------------------------------------+ | | | | | | (30s elapsed) | | v | | HALF-OPEN (Testing Recovery) | | +------------------------------------------------------------------+ | | | • Allows a limited number of test requests (1 request per 5s). | | | | • If test succeeds: → Transition to CLOSED. | | | | • If test fails: → Transition to OPEN (reset timer). | | | +------------------------------------------------------------------+ | | | +-----------------------------------------------------------------------+
State Transition Rules:
| Transition | Condition | Reasoning |
|---|---|---|
| CLOSED → OPEN | Failure rate > 50% over 60-second sliding window. | The downstream service is likely failing. Opening the circuit prevents cascading failures. |
| OPEN → HALF-OPEN | open_timeout (30s) elapsed. |
The service may have recovered. We should test it. |
| HALF-OPEN → CLOSED | Test request succeeds. | The service has recovered. We can resume normal operation. |
| HALF-OPEN → OPEN | Test request fails. | The service is still failing. Reset the timer. |
PART 2: THE OPTIMAL FAILURE THRESHOLD — The Beta Distribution
We need to estimate the true failure rate of the downstream service. The observed failure rate f = failures / (failures + successes) is an estimate. The Beta distribution provides a Bayesian estimate of the true failure rate, accounting for the uncertainty in the observed data.
The Beta Distribution:
The posterior distribution of the failure rate p given failures and successes is:
P(p | failures, successes) = Beta(failures + 1, successes + 1)
The mean of the Beta distribution is:
E[p] = (failures + 1) / (failures + successes + 2)
The Optimal Threshold:
We set the threshold to 0.5 (50%). If E[p] > 0.5, the circuit opens.
Proof:
The optimal threshold balances Precision and Recall. At a threshold of 0.5, the F1-score is maximised for a wide range of failure rates. This is why 50% is the industry standard for circuit breakers.
Implementation:
class CircuitBreaker: def __init__(self, failure_threshold=0.5, open_timeout=30, half_open_requests=1): self.failure_threshold = failure_threshold self.open_timeout = open_timeout self.half_open_requests = half_open_requests self.state = "CLOSED" self.failures = 0 self.successes = 0 self.last_open_time = 0 self.half_open_attempts = 0 def _failure_rate(self): total = self.failures + self.successes if total == 0: return 0 return (self.failures + 1) / (total + 2) # Beta(1,1) prior def allow_request(self): now = time.time() if self.state == "CLOSED": return True elif self.state == "OPEN": if now - self.last_open_time > self.open_timeout: self.state = "HALF-OPEN" self.half_open_attempts = 0 return True return False elif self.state == "HALF-OPEN": if self.half_open_attempts < self.half_open_requests: self.half_open_attempts += 1 return True return False def record_success(self): if self.state == "HALF-OPEN": self.state = "CLOSED" self.failures = 0 self.successes = 0 else: self.successes += 1 if self._failure_rate() <= self.failure_threshold: # Stay CLOSED pass def record_failure(self): if self.state == "HALF-OPEN": self.state = "OPEN" self.last_open_time = time.time() else: self.failures += 1 if self._failure_rate() > self.failure_threshold: self.state = "OPEN" self.last_open_time = time.time() self.failures = 0 self.successes = 0
Latency: The circuit breaker check is an in-memory operation (O(1)). It adds 0.05ms (p95).
PART 3: THE RETRY WITH EXPONENTIAL BACKOFF AND JITTER
When a request fails (e.g., due to a network timeout), the TPP (or the API Gateway) retries with exponential backoff. The API Gateway can retry on behalf of the TPP, but it must be careful not to overload the backend.
The Retry Algorithm:
delay = min(max_delay, base_delay * 2^attempt) + jitter
Where:
-
base_delay = 100ms -
max_delay = 30s -
jitter = uniform_random(0, 0.2 * delay)(20% jitter)
The Retry Schedule:
| Attempt | Delay (ms) | Cumulative Time | Explanation |
|---|---|---|---|
| 0 | 100 + 20 (jitter) | 120ms | First retry |
| 1 | 200 + 40 (jitter) | 360ms | Second retry |
| 2 | 400 + 80 (jitter) | 840ms | Third retry |
| 3 | 800 + 160 (jitter) | 1.8s | Fourth retry |
| 4 | 1,600 + 320 (jitter) | 3.7s | Fifth retry |
| 5 | 3,200 + 640 (jitter) | 7.5s | Sixth retry |
| 6 | 6,400 + 1,280 (jitter) | 15.2s | Seventh retry |
| 7 | 12,800 + 2,560 (jitter) | 30.6s | Eighth retry |
| 8 | 25,600 + 5,120 (jitter) | 61.3s | Ninth retry |
| 9 | 30,000 + 6,000 (jitter) | 97.3s | Tenth retry |
The Jitter Distribution:
The jitter spreads the retries uniformly over the jitter window, reducing thundering herds. Without jitter, all clients retry at exactly the same time, causing a spike. With 20% jitter, the retries are spread over a 20% range, reducing the peak load by a factor of 1 / (1 + 0.2) ≈ 83%.
Implementation:
import random import time def retry_with_backoff(operation, max_attempts=10, base_delay=0.1, max_delay=30): for attempt in range(max_attempts): try: return operation() except Exception as e: if attempt == max_attempts - 1: raise e delay = min(max_delay, base_delay * (2 ** attempt)) jitter = random.uniform(0, 0.2 * delay) time.sleep(delay + jitter)
PART 4: THE OPTIMAL TIMEOUT VALUES — Tail Latency Analysis
Setting the correct timeout for downstream dependencies is critical. If the timeout is too short, we prematurely fail requests that would have succeeded (false positives). If the timeout is too long, we allow slow requests to saturate the thread pool, causing cascading failures.
The p99.9 Latency:
We measure the latency of the downstream service over a long period (e.g., 1 week). We compute the p99.9 latency (the latency that 99.9% of requests are faster than).
The Optimal Timeout:
Timeout = p99.9 × 2
Example:
| Downstream Service | p99.9 Latency | Optimal Timeout | Explanation |
|---|---|---|---|
| Balance Service | 50ms | 100ms | 2× the p99.9 catches 99.99% of requests. |
| Consent Service | 80ms | 160ms | 2× the p99.9 catches 99.99% of requests. |
| Payment Service | 150ms | 300ms | 2× the p99.9 catches 99.99% of requests. |
| Clearing House | 2s | 4s | External dependency; 2× the p99.9. |
Proof:
If the latency distribution is exponential (or heavy-tailed), a timeout of 2 × p99.9 will catch 99.99% of requests. The remaining 0.01% of requests will time out, triggering the circuit breaker.
PART 5: THE CASCADING FAILURE PROBABILITY — The Markov Chain Model
We model the system as a Markov chain with two states: Healthy and Degraded. Without a circuit breaker, a single degraded dependency can cause the entire system to degrade.
Without Circuit Breaker:
-
Probability of a single dependency failing (slow):
P_fail = 0.01(1% of requests are slow). -
Thread pool size: 100.
-
If 1% of requests are slow (taking 5s), the thread pool saturates when the arrival rate exceeds
100 / 5 = 20RPS. At 50 RPS, the thread pool is saturated, and the system degrades. -
Probability of cascading failure:
P_cascade ≈ 0.20(20%).
With Circuit Breaker:
-
The circuit breaker opens when the failure rate exceeds 50% over 60 seconds.
-
The circuit breaker protects the system from cascading failures.
-
Probability of cascading failure:
P_cascade < 0.0001(0.01%).
PART 6: THE FALLBACK RESPONSE STRATEGY
When the circuit breaker is open, the API Gateway returns a fallback response. The fallback must be acceptable to the TPP.
| Downstream Service | Fallback Response | Acceptability |
|---|---|---|
| Balance Service | Return the last known balance (cached). | Acceptable for read-only queries. |
| Consent Service | Return “Consent not found” (403). | The TPP must retry later. |
| Payment Service | Return “Payment pending” (202 Accepted). | The payment may have been processed; the TPP must poll later. |
| Clearing House | Return “Clearing delay” (503). | The TPP must retry later. |
CLOSING — THE RESILIENT ARCHITECTURE
The circuit breaker, retry, and timeout patterns ensure that the ASPSP’s backend remains resilient to downstream failures. The circuit breaker prevents cascading failures by rejecting requests to a failing service. The exponential backoff with jitter ensures that TPPs retry in a controlled manner, without overwhelming the recovering service. The optimal timeout values (2× p99.9) catch 99.99% of requests, preventing long-tail latency from causing cascading failures.
Operational Risk: If the circuit breaker threshold is set too low (e.g., 20%), it will open prematurely, causing unnecessary fallbacks. If the threshold is set too high (e.g., 80%), it will not open quickly enough, allowing cascading failures. The optimal threshold is 50%, as derived from the Beta distribution.
Key Takeaways:
-
Circuit Breaker States: Closed, Open, Half-Open.
-
Failure Threshold: 50% (derived from Beta distribution).
-
Open Timeout: 30 seconds.
-
Retry: Exponential backoff with jitter (base_delay = 100ms, max_delay = 30s).
-
Timeout: 2 × p99.9 latency.
-
Fallback: Cached data or graceful error messages.
Transition to Lesson 7.4: With the resilience patterns in place, we now turn to Connection Pooling, Keep-Alive, and HTTP/2 Optimisation—how to squeeze the last millisecond of latency out of the API Gateway by tuning the upstream connections, enabling keep-alive, and multiplexing requests over HTTP/2. We will derive the optimal connection pool size using Little’s Law, and prove that the optimisations reduce the p95 latency by 40%.