Building Reliable Systems: Core Reliability Patterns Explained
Production reliability therefore depends on several complementary patterns: timeouts bound waiting, retries recover from transient failures, circuit breakers stop repeatedly calling unhealthy dependencies, bulkheads isolate resources, load shedding protects capacity, graceful degradation preserves critical functionality, and health checks keep unhealthy instances away from traffic.
Table of Contents
- Reliability Is Failure Management
- Timeouts and Retries
- Failure Isolation and Overload Protection
- Graceful Degradation and Fallbacks
- Health Detection and Recovery
- Production Design Example
- Common Mistakes
- Production Checklist
- Conclusion
- More Articles to Read
Reliability Is Failure Management
Production systems contain many components that can fail independently. An application can remain healthy while its database becomes overloaded. One availability zone can fail while others continue operating. A payment provider can become slow while the rest of an e-commerce platform remains available.
The architecture should therefore assume that partial failure is normal. The objective is to limit the blast radius, preserve critical functionality, and recover without turning a local failure into a cascading outage.
Normal operation
Client
|
v
Service A
/ \
v v
B C
| |
v v
DB D
Dependency D fails
Client
|
v
Service A -------- remains available
/ \
v v
B C -------- degrades safely
| |
v X
DB D
The difference between a small dependency incident and a platform outage is often determined by how Service C handles that failure.
Reliability, Availability, and Resilience
These terms describe related but different properties.
| Property | Meaning | Example |
|---|---|---|
| Reliability | Ability to perform expected behavior correctly over time | Orders are processed without loss or duplication |
| Availability | Ability to accept and serve requests when needed | Order API remains reachable 99.95% of the month |
| Resilience | Ability to tolerate failures and continue operating or recover | Checkout continues when recommendations fail |
A system can be available but unreliable. An API returning HTTP 200 while occasionally losing orders has high technical availability but poor reliability.
Likewise, redundancy alone does not guarantee resilience. If every replica depends on the same saturated database or external provider, adding application instances does not remove the common failure point.
Reliability must be evaluated at the level of the business operation, not only individual infrastructure components.
Timeouts and Retries
Remote calls have uncertain completion time. A dependency may respond in 20 milliseconds, respond in 20 seconds, or never produce a usable response before the caller itself fails.
Timeouts and retries address different parts of this problem. Timeouts limit how long resources remain committed to uncertain work; retries provide another opportunity for transient failures to succeed.
Timeouts Bound Failure Duration
Every remote operation should have a finite deadline. Without one, slow dependencies can consume request workers, threads, connections, memory, and database resources until the caller becomes unavailable as well.
Incoming requests
|
v
Application
|
| dependency becomes slow
v
Downstream Service
Without bounded timeouts:
Request 1 -------- waiting ----------------------->
Request 2 -------- waiting ----------------------->
Request 3 -------- waiting ----------------------->
Request 4 -------- waiting ----------------------->
Request 5 -------- waiting ----------------------->
Worker / connection pool becomes exhausted
A timeout should be derived from the end-to-end request budget rather than selected independently for every dependency.
If checkout has a 1-second latency target, allowing three downstream calls to wait five seconds each is inconsistent with that target.
Checkout latency budget: 1000 ms
Gateway + network 80 ms
Application logic 70 ms
Inventory 180 ms
Pricing 120 ms
Payment 300 ms
Database 80 ms
Safety margin 170 ms
-----------------------------
Total 1000 ms
The exact allocation depends on the workload, but the important rule is that dependency deadlines must fit inside the caller's deadline.
Retries Handle Transient Failures
Retries are useful when another attempt has a reasonable probability of succeeding: temporary network errors, connection resets, short-lived throttling, or failover between replicas.
They are usually inappropriate for validation failures, authentication errors, permanent business rejections, or deterministic application bugs.
import asyncio
import random
import httpx
async def get_inventory(product_id: str) -> dict:
timeout = httpx.Timeout(connect=0.3, read=0.7, write=0.5, pool=0.3)
async with httpx.AsyncClient(timeout=timeout) as client:
for attempt in range(3):
try:
response = await client.get(
f"http://inventory-service/v1/items/{product_id}"
)
response.raise_for_status()
return response.json()
except (httpx.TimeoutException, httpx.NetworkError):
if attempt == 2:
raise
# Exponential backoff spreads repeated attempts over time.
# Jitter prevents many callers from retrying simultaneously.
delay = (0.1 * (2 ** attempt)) + random.uniform(0, 0.05)
await asyncio.sleep(delay)
raise RuntimeError("unreachable")
Retries consume additional capacity and can make an overloaded dependency even less likely to recover. They should therefore have bounded attempts, exponential backoff, jitter, and an overall deadline.
Side-effecting operations also need idempotency. If a payment request times out after the provider charged the customer, blindly retrying the operation can create a duplicate charge.
Timeout selection, retry classification, backoff, jitter, and retry amplification are covered in depth in Timeouts, Retries, and Exponential Backoff.
Failure Isolation and Overload Protection
Timeouts limit individual calls, but a large number of failing calls can still consume significant resources. Reliability therefore requires mechanisms that prevent unhealthy dependencies and excessive workloads from exhausting shared capacity.
Circuit breakers, bulkheads, and load shedding address different failure modes and are commonly used together.
Circuit Breakers
A circuit breaker observes dependency failures and temporarily stops sending requests when the dependency appears unhealthy.
CLOSED
Requests allowed
|
| failures exceed threshold
v
OPEN
Requests fail fast
|
| recovery interval
v
HALF-OPEN
Limited probe requests
/ \
/ \
success failure
| |
v v
CLOSED OPEN
Failing fast protects caller resources and gives the downstream dependency time to recover. It can also reduce useless retries against a dependency already known to be unhealthy.
Circuit breakers work best for failures that persist long enough to justify temporarily avoiding a dependency. They provide less value for isolated random failures.
Bulkheads
A bulkhead divides resources so one dependency or workload cannot consume all available capacity.
Consider an API using the same 100 outbound connections for Payments, Recommendations, and Search. If Recommendations becomes slow and occupies all 100 connections, payment requests can fail even though Payments itself remains healthy.
Separate pools isolate the failure:
Application
Payment Pool
[20 connections]
|
v
Payment Service
Recommendation Pool
[30 connections]
|
X
Recommendation Service
Search Pool
[50 connections]
|
v
Search Service
The recommendation failure remains constrained to its allocated capacity.
Bulkheads can isolate thread pools, connection pools, queues, worker groups, concurrency limits, containers, availability zones, or entire service instances.
Load Shedding
Load shedding rejects work when the system cannot process it safely. This can appear counterintuitive, but accepting unlimited requests during overload often causes every request to become slow or fail.
Capacity: 1,000 requests/sec
Incoming traffic: 1,800 requests/sec
Without load shedding:
1,800 req/s
|
v
Growing queues
|
v
High latency
|
v
Timeouts
|
v
Retries
|
v
More traffic
|
v
Collapse
With load shedding:
1,800 req/s
|
+---- 1,000 accepted
|
+------ 800 rejected quickly
The accepted requests retain a better chance of completing within their latency targets.
Systems can shed traffic using concurrency limits, bounded queues, rate limits, priority classes, admission control, or explicit overload responses.
Circuit breakers protect callers from unhealthy dependencies, bulkheads isolate resource consumption, and load shedding protects the system from excessive work. A deeper comparison is available in Circuit Breaker vs Bulkhead vs Load Shedding.
Graceful Degradation and Fallbacks
Not every dependency is equally important. A checkout operation may require inventory and payment processing but not recommendations, reviews, analytics, or personalized promotions.
Graceful degradation preserves essential functionality while temporarily removing or simplifying optional functionality.
Product Page
|
+--> Product Data -------- REQUIRED
|
+--> Price --------------- REQUIRED
|
+--> Inventory ----------- REQUIRED
|
+--> Reviews ------------- OPTIONAL
|
+--> Recommendations ----- OPTIONAL
|
+--> Personalization ----- OPTIONAL
If Recommendations fails, returning a product page without recommendations is usually preferable to returning an error for the entire page.
Classify Critical and Optional Dependencies
Dependencies should be classified according to the business operation rather than globally.
Inventory may be critical for checkout but optional for a search-result page. Customer personalization may be useful for browsing but irrelevant for payment processing.
Common degradation strategies include:
- Omit optional content: render the page without recommendations or reviews.
- Use cached data: serve slightly stale catalog or configuration information.
- Use default behavior: fall back to non-personalized results.
- Disable expensive features: temporarily stop optional processing during overload.
- Queue work for later: accept non-critical asynchronous work without processing it immediately.
- Return partial responses: expose available data while marking unavailable sections explicitly.
Fallbacks must remain semantically safe. Serving a cached product description may be acceptable; serving stale authorization, inventory, account balance, or payment state can create incorrect business decisions.
Graceful degradation should therefore be designed around business correctness, not merely technical availability. More detailed strategies are covered in Designing Graceful Degradation Strategies.
Health Detection and Recovery
Reliability mechanisms require accurate information about whether an instance can receive traffic and whether it can recover automatically. A process being alive does not necessarily mean it is ready to serve production requests.
Health signals should distinguish between process failure, temporary startup state, dependency degradation, and inability to serve traffic.
Health, Readiness, and Liveness
Different health checks answer different questions.
| Check | Question | Typical Action |
|---|---|---|
| Health | What is the current operational state? | Monitoring and diagnostics |
| Readiness | Can this instance receive traffic? | Add or remove from traffic |
| Liveness | Is the process stuck and unable to recover itself? | Restart the instance |
A service starting with an empty connection pool may be alive but not ready. A service whose optional recommendation dependency is unavailable may still be ready if it can serve core requests in degraded mode.
Liveness checks should be conservative. Restarting a healthy application because an external database is temporarily unavailable can turn a dependency incident into a restart storm.
Probe semantics, Kubernetes behavior, dependency checks, startup handling, and common probe failures are covered in Health Checks, Readiness, and Liveness Probes.
Design for Recovery
Failure handling does not end when traffic is rejected or a dependency is isolated. The system must eventually return to normal operation and repair incomplete work.
Recovery mechanisms can include:
- automatic instance replacement
- replica or availability-zone failover
- message redelivery
- dead-letter processing
- workflow resumption
- data reconciliation
- projection rebuilding
- backup restoration
Long-running workflows should persist enough state to continue after a worker or service restarts. Critical recovery should not depend entirely on in-memory state.
Recovery also needs bounded automation. Automatic failover that repeatedly moves traffic between two unhealthy systems can make diagnosis and stabilization harder.
Recovery workflows, reconciliation, failover, replay, and restoration are covered further in Failure Recovery in Distributed Systems.
Production Design Example
Consider an e-commerce checkout service that depends on Inventory, Payments, Recommendations, and an asynchronous Notification system. These dependencies have different business importance and therefore should not share identical failure behavior.
The objective is not to make every dependency permanently available. The objective is to preserve correct checkout behavior when individual components become slow or unavailable.
Reliable Checkout Service
Client
|
v
API Gateway
|
Load Shedding
|
v
Checkout Service
/ | \
/ | \
v v v
Inventory Payments Recommendations
REQUIRED REQUIRED OPTIONAL
| | |
Timeout Timeout Timeout
Retry Retry Circuit Breaker
Bulkhead Bulkhead Bulkhead
| | |
v v X
Healthy Healthy Unavailable
\ /
\ /
v v
Order Accepted
|
v
Message Broker
|
v
Notifications
The gateway applies rate limits and request-size controls before traffic reaches Checkout. Checkout itself has a concurrency limit so sudden traffic spikes cannot consume unlimited workers or database connections.
Inventory and Payments are critical dependencies. Their calls use short deadlines and isolated connection pools. Transient network failures may be retried within the overall checkout deadline.
Payment creation uses an idempotency key derived from the logical checkout operation:
import httpx
async def authorize_payment(
order_id: str,
amount: int,
idempotency_key: str,
) -> dict:
async with httpx.AsyncClient(timeout=1.0) as client:
response = await client.post(
"http://payment-service/v1/authorizations",
headers={
# Retries reuse this value so an ambiguous timeout
# cannot create another logical authorization.
"Idempotency-Key": idempotency_key,
},
json={
"order_id": order_id,
"amount": amount,
},
)
response.raise_for_status()
return response.json()
Recommendations are optional. If the recommendation service becomes slow, its timeout expires quickly and checkout continues without personalized offers. Repeated failures can open a circuit breaker so future requests stop spending latency on a dependency already known to be unhealthy.
Bulkheads prevent Recommendation requests from consuming connections reserved for Inventory or Payments. The failure remains isolated even if the recommendation service stops responding completely.
Notification delivery is removed from the synchronous checkout path. After the order commits, a durable event is published and Notification processes it asynchronously. Temporary Notification downtime therefore creates queue lag instead of checkout failure.
If system capacity becomes exhausted, low-priority or excessive requests are rejected before the platform enters uncontrolled saturation. Critical operations receive protected capacity where the infrastructure supports priority-based admission.
Operational monitoring should expose both component and business behavior:
Checkout
request rate
p95 / p99 latency
error rate
concurrency saturation
Inventory
latency
timeout rate
retry rate
connection pool saturation
Payments
authorization success rate
timeout rate
idempotent replay rate
provider latency
Recommendations
circuit state
fallback rate
timeout rate
Notifications
queue depth
oldest message age
processing rate
A rising recommendation fallback rate indicates degraded functionality but not necessarily failed checkout. A rising payment timeout rate directly threatens the critical business operation and deserves a different operational response.
This distinction is central to reliability engineering: failure severity should be determined by business impact, not simply by whether a component reports an error.
Common Mistakes
Reliability mechanisms can themselves cause outages when applied without explicit limits or without understanding the business operation they are protecting.
| Mistake | Why It Causes Problems | Better Approach |
|---|---|---|
| Using very long or missing timeouts | Slow dependencies hold workers and connections until the caller becomes saturated. | Derive bounded dependency deadlines from the end-to-end latency budget. |
| Retrying every failure | Permanent errors are repeated and overloaded dependencies receive even more traffic. | Retry only failures likely to be transient. |
| Retrying at every service layer | Nested retry policies can multiply a single request into many downstream attempts. | Coordinate retry responsibility and maintain one overall request budget. |
| Retrying side effects without idempotency | Ambiguous failures can duplicate payments, orders, reservations, or messages. | Define stable idempotency semantics before retrying mutating operations. |
| Sharing one resource pool across all dependencies | One slow dependency can consume resources needed by healthy critical dependencies. | Use bulkheads for workloads with different failure and priority characteristics. |
| Using unbounded queues | Overload becomes growing memory usage and extreme latency instead of visible rejection. | Use bounded queues and explicit overload behavior. |
| Making every dependency mandatory | Failures in optional functionality unnecessarily make critical workflows unavailable. | Classify dependencies and define safe degraded behavior. |
| Using stale fallback data without semantic analysis | Availability improves while business correctness can silently fail. | Use cached fallbacks only for data whose staleness is explicitly acceptable. |
| Putting dependency checks in aggressive liveness probes | An external outage can restart every healthy application instance simultaneously. | Restart only when the process itself cannot recover. |
| Autoscaling without understanding bottlenecks | Additional instances can increase pressure on an already saturated database or provider. | Scale according to the constrained resource and complete capacity model. |
| Monitoring infrastructure instead of business outcomes | Components can appear healthy while orders, payments, or workflows are failing. | Measure end-to-end success, latency, degradation, and recovery. |
| Testing only normal operation | Failure-handling paths remain unverified until a real production incident occurs. | Exercise timeouts, overload, dependency loss, retries, failover, and recovery before incidents. |
Production Checklist
Reliability should be designed into dependency calls, resource allocation, deployment behavior, and recovery procedures rather than added after the first outage.
- Define critical business operations: identify which workflows must remain available and which features can degrade.
- Map runtime dependencies: document the services, databases, brokers, caches, and external providers required by each critical operation.
- Set explicit timeouts: ensure every remote call has a deadline consistent with the caller's latency budget.
- Classify retryable failures: retry only transient conditions and keep attempts inside the overall deadline.
- Add exponential backoff and jitter: prevent retry traffic from immediately returning as another synchronized spike.
- Protect side effects with idempotency: make ambiguous retries safe for payments, reservations, orders, and similar operations.
- Isolate critical resources: use separate pools or concurrency limits where one dependency could starve another.
- Bound queues and concurrency: make overload visible instead of accumulating unlimited work.
- Define load-shedding behavior: decide which traffic should be rejected first when capacity is exhausted.
- Design safe fallbacks: specify which data can be stale, omitted, defaulted, or processed asynchronously.
- Separate readiness from liveness: remove instances from traffic without restarting processes that can recover normally.
- Measure degradation: monitor fallback rate, circuit state, retry rate, timeout rate, and rejected work.
- Monitor saturation: track workers, threads, connections, queues, memory, CPU, and downstream capacity before exhaustion.
- Persist recoverable workflows: ensure critical operations can resume after process, node, or dependency failure.
- Test failure scenarios: validate behavior under slow dependencies, outages, overload, message delays, restarts, and recovery.
Conclusion
Reliable systems are built by assuming components will fail and controlling what happens next. Timeouts bound waiting, retries recover transient failures, circuit breakers prevent repeated calls to unhealthy dependencies, bulkheads isolate resources, load shedding protects capacity, graceful degradation preserves essential functionality, and health checks support safe traffic management and recovery.
No individual pattern creates reliability. The patterns work together to keep failures bounded, observable, recoverable, and proportional to the component that actually failed.
Key Takeaway
Design reliability around failure containment rather than failure prevention alone. Bound every remote operation, isolate critical resources, reject unsustainable work, degrade optional functionality safely, detect unhealthy instances accurately, and ensure important workflows can recover after interruptions.
Comments (0)