Backend Engineering · 2 of 2
Build fault-tolerant systems using circuit breakers, timeouts, retries, bulkheads, and structured error handling.
In distributed systems, failures are inevitable. Network timeouts, service crashes, database overloads—these are not exceptions but normal occurrences. Resilience patterns help you build systems that gracefully degrade and recover.
Without proper error handling, a single failure can cascade through your entire system:
Service A ---> Service B (SLOW) ---> Service C
↓ ↓
Queues fill up, threads exhaust
↓
Service A becomes unresponsive
↓
Upstream services also fail
This is the Thundering Herd problem.
Set a maximum wait time for external calls. If the response doesn't arrive, fail fast.
def get_user(user_id: int) -> User:
try:
return requests.get(
f"http://user-service/{user_id}",
timeout=2.0 # 2 seconds
)
except requests.Timeout:
return handle_timeout_error()
Timeouts per Layer:
Transient failures (network blip, temporary overload) often resolve quickly. Retry with exponential backoff.
@retry(max_attempts=3, backoff_factor=2)
def call_external_service():
return requests.get("http://external-api")
# Retry timeline:
# Attempt 1 (fails)
# Wait 1 second
# Attempt 2 (fails)
# Wait 2 seconds
# Attempt 3 (fails)
# Raise error
Exponential Backoff Formula: Exponential Backoff Formula:
Wait Time = base * 2^(attempt) + jitter
Jitter: Add randomness to prevent thundering herd (all clients retrying at the same time).
When NOT to retry:
Stop calling a failing service to prevent cascading failures.
States:
Closed (Working)
↓ (5 failures)
Open (Failing)
↓ (Wait 60 sec)
Half-Open (Testing)
↓ (Request succeeds)
Closed (Recovered)
Config:
Isolate critical resources to prevent a failure in one area from affecting others.
Thread Pool Isolation:
Request 1 -> [ThreadPool A] -> Service A
Request 2 -> [ThreadPool B] -> Service B
Request 3 -> [ThreadPool C] -> Service C
If Service A is slow, only ThreadPool A's threads are exhausted. ThreadPool B and C remain available for Services B and C.
Isolation Levels:
Reject excess requests gracefully to protect your service.
Strategies:
Load Shedding: When overloaded, reject low-priority requests to keep the system responsive for critical operations.
if queue_depth > threshold:
if request.priority == "low":
return 503 Service Unavailable # Shed load
else:
queue_request() # Accept high-priority
{
"error": {
"code": "SERVICE_UNAVAILABLE",
"message": "Payment service is temporarily down",
"retryable": true,
"retry_after_seconds": 30,
"trace_id": "abc123"
}
}
Track:
Alert on:
Get an email when I publish the next chapter. No spam — just new technical deep-dives.
Comments
Share feedback or questions about this blog post.
No comments yet. Be the first to share your thoughts.