Reliability in Software

By Alex Snowgirl — Published on
0 Likes
0 Dislikes
Reliability in Software
Reliability in Software

Reliability is the ability of a software system to keep doing the correct thing over time, even when parts of the system fail.

A reliable system does not need to be perfect. Servers crash. Networks become slow. Databases restart. Third-party APIs return errors. Deployments introduce bugs. Traffic suddenly increases.

The real question is:

What happens when something goes wrong?

If one failed dependency causes the entire application to stop working, the system is fragile.

If the system detects the problem, limits the damage, recovers automatically, and continues serving most users, it is reliable.

This article explains reliability using simple examples and practical patterns commonly used in production systems.

Table of Contents

What Is Reliability?

Imagine a very small application:

User
 │
 ▼
Application
 │
 ▼
Database

When everything is healthy, the system works.

But suppose the database becomes unavailable for 30 seconds.

A fragile application may behave like this:

Database unavailable
        ↓
Every request fails
        ↓
Users retry manually
        ↓
Traffic increases
        ↓
Application becomes overloaded

A more reliable system may behave differently:

Database temporarily unavailable
        ↓
Requests fail fast
        ↓
Retries are controlled
        ↓
Some cached data remains available
        ↓
Database recovers
        ↓
System returns to normal

The failure still happened. Reliability comes from how the system reacted to it.

A useful way to think about reliability is:

Reliable system
=
fewer failures
+
smaller failure impact
+
faster recovery

Reliability vs Availability

Availability usually describes whether the system can serve requests at a particular moment.

Reliability is broader. It includes whether the system behaves correctly and consistently over time.

For example, imagine a payment API that is always reachable but occasionally charges customers twice.

Availability: high
Reliability: poor

Now imagine another service that experiences a short outage once a year but otherwise processes every request correctly.

Its reliability may still be much better.

Availability is therefore an important part of reliability, but reliability also includes correctness, recovery, durability, and predictable behavior during failures.

Reliability vs Performance

A system can be fast but unreliable.

Consider two APIs:

API A
Latency: 20 ms
Failure rate: 8%

API B
Latency: 60 ms
Failure rate: 0.01%

API A is faster when it works, but API B is likely more useful in production.

Reliability engineering often accepts a little extra latency or infrastructure cost in exchange for fewer failures.

Examples include:

  • replicating data
  • writing to durable queues
  • performing health checks
  • keeping spare capacity
  • validating requests

Failures Are Normal

Reliable systems are designed with one assumption:

Something will eventually fail.

Possible failures include:

Server crashes
Network packets disappear
Database becomes overloaded
DNS lookup fails
Disk fills up
Memory is exhausted
Deployment contains a bug
Certificate expires
Third-party API slows down
Cloud region experiences problems
Message is delivered twice
Worker crashes halfway through a job

A small system may experience these failures rarely. A large distributed system experiences some form of failure almost continuously because it contains many more components.

Suppose one server has a 0.1% chance of failure during a particular period.

With one server, failure is uncommon.

With thousands of servers, the probability that some server is failing becomes much higher.

This is why production architectures should not depend on every component always working perfectly.

Redundancy

One of the simplest reliability techniques is redundancy: do not depend on only one copy of a critical component.

Consider one application server:

Users
  │
  ▼
App Server

If that server crashes:

Users
  │
  ▼
   X

The application becomes unavailable.

Now consider three servers:

                    ┌──► App 1
Users ──► Load Balancer
                    ├──► App 2
                    └──► App 3

If App 2 fails:

App 1 ✓
App 2 ✗
App 3 ✓

The load balancer can stop sending traffic to App 2 while the other servers continue processing requests.

The same idea applies to databases:

Primary Database
       │
       ├──► Replica 1
       └──► Replica 2

If the primary fails, one replica may be promoted depending on the database architecture.

Redundancy reduces single points of failure, but it introduces new questions:

How is failure detected?
How quickly can traffic move?
Is replicated data current?
What happens during failover?
Can both nodes accidentally become primary?

Redundancy helps only when the failover mechanism itself is reliable.

Timeouts

One of the most important reliability rules in distributed systems is:

Do not wait forever.

Imagine Service A calls Service B:

Service A ─────► Service B

Normally Service B responds in 100 ms.

Then Service B becomes overloaded and requests start taking 60 seconds.

Without a timeout:

Request 1 waiting...
Request 2 waiting...
Request 3 waiting...
Request 4 waiting...
Request 5 waiting...
...

Threads, connections, memory, and request slots remain occupied.

Eventually Service A can fail even though Service A itself was healthy.

With a timeout:

response = http_client.get(
    "https://inventory-service/items/123",
    timeout=2.0
)

Service A decides that after two seconds, waiting is no longer useful.

The request fails quickly and resources are released.

Timeouts therefore prevent slow dependencies from consuming unlimited resources.

But timeout values matter.

Timeout too short
→ healthy requests fail unnecessarily

Timeout too long
→ resources remain blocked too long

A reasonable timeout should be based on expected dependency latency and the latency budget of the caller.

For a deeper explanation, see Timeouts, Retries, and Exponential Backoff.

Retries

Some failures are temporary.

For example:

Request
  │
  ▼
Database connection fails
  │
  ▼
Connection recovers 200 ms later

Retrying may make the request succeed.

A simple retry flow:

Attempt 1 → failed
Wait
Attempt 2 → failed
Wait
Attempt 3 → success

Retries are useful for transient failures such as:

  • temporary network errors
  • short service interruptions
  • rate limiting
  • leader elections
  • temporary resource contention

But uncontrolled retries are dangerous.

Suppose a service normally receives 10,000 requests per second.

The dependency starts failing and every request retries three times:

Original traffic: 10,000 req/sec

3 retries:
10,000 + 30,000

Total potential traffic:
40,000 req/sec

The retry mechanism can turn a partial outage into a much larger outage.

Exponential Backoff

Instead of retrying immediately:

retry
retry
retry
retry

increase the delay between attempts:

Attempt 1
   ↓
wait 100 ms

Attempt 2
   ↓
wait 200 ms

Attempt 3
   ↓
wait 400 ms

Attempt 4
   ↓
wait 800 ms

This gives the failing dependency time to recover.

Random jitter is often added so that thousands of clients do not retry at exactly the same moment.

import random
import time

delay = 0.2

for attempt in range(4):
    try:
        call_service()
        break
    except TemporaryError:
        jitter = random.uniform(0, 0.1)
        time.sleep(delay + jitter)
        delay *= 2

Idempotency

Retries become dangerous when an operation has side effects.

Consider a payment request:

Client
  │
  ▼
Charge $100
  │
  ▼
Payment succeeds
  │
  X response lost

The client does not know that the payment succeeded.

It retries:

Charge $100 again

Without protection, the customer may be charged twice.

An idempotency key allows the server to recognize repeated attempts:

headers = {
    "Idempotency-Key": "order-81293-payment"
}

The server stores the result of the first successful operation:

order-81293-payment
    ↓
already processed
    ↓
return previous result

This turns retrying into a safe operation.

Circuit Breakers

A timeout stops one request from waiting forever.

A circuit breaker stops the application from repeatedly calling a dependency that is already known to be unhealthy.

Imagine:

Recommendation Service
        │
        ▼
External ML API

The external API becomes unavailable.

Without a circuit breaker:

Request 1 → wait → fail
Request 2 → wait → fail
Request 3 → wait → fail
Request 4 → wait → fail
...

Every request pays the timeout cost.

With a circuit breaker:

Several failures detected
        ↓
Circuit opens
        ↓
New requests fail immediately
        ↓
Wait for recovery period
        ↓
Try dependency again

The states are usually described as:

CLOSED
  │
  │ too many failures
  ▼
OPEN
  │
  │ recovery timeout
  ▼
HALF-OPEN
  │
  ├── success → CLOSED
  └── failure → OPEN

This protects both systems:

The caller avoids wasting resources, while the unhealthy dependency receives fewer requests while recovering.

Bulkheads

A bulkhead prevents one failing workload from consuming all resources.

The name comes from ships. A ship is divided into compartments so that flooding one compartment does not sink the entire ship.

The same concept applies to software.

Suppose one worker pool processes:

Emails
Invoices
Image processing
Webhooks
Reports

A huge report suddenly consumes every worker:

Worker 1 → report
Worker 2 → report
Worker 3 → report
Worker 4 → report
Worker 5 → report

Now emails and webhooks cannot run.

Instead, isolate workloads:

Email Workers
├── Worker 1
└── Worker 2

Report Workers
├── Worker 3
└── Worker 4

Webhook Workers
└── Worker 5

A report overload now affects report processing, but other workloads remain operational.

Bulkheads can be implemented using:

  • separate thread pools
  • separate connection pools
  • separate queues
  • separate worker groups
  • separate service instances
  • resource quotas

Graceful Degradation

Reliable systems do not always need every feature to work.

Consider an online store:

Product page
├── Product details
├── Price
├── Inventory
├── Reviews
├── Recommendations
└── Recently viewed items

Suppose the recommendation service fails.

A fragile implementation may return:

500 Internal Server Error

for the entire product page.

A better implementation may show:

Product details ✓
Price ✓
Inventory ✓
Reviews ✓
Recommendations unavailable
Recently viewed ✓

The important part of the application continues working.

This is called graceful degradation.

Other examples:

Search ranking service fails
→ return lexical search results

Personalization unavailable
→ show popular products

Analytics system unavailable
→ buffer events and continue checkout

Image resize service unavailable
→ serve original image

Real-time inventory temporarily unavailable
→ display last known value with warning

The goal is to identify which features are critical and which can temporarily degrade.

Queues and Failure Isolation

Queues can dramatically improve reliability by separating request processing from background work.

Consider order creation:

Create Order
   │
   ├──► Send email
   ├──► Update CRM
   ├──► Generate invoice
   ├──► Send analytics
   └──► Notify warehouse

If all operations happen synchronously, one failing dependency can break the whole request.

A queue changes the design:

Client
  │
  ▼
Order API
  │
  ├──► Database
  │
  └──► Queue
         │
         ├──► Email Worker
         ├──► Invoice Worker
         ├──► CRM Worker
         └──► Warehouse Worker

If the CRM system is unavailable:

CRM job
  ↓
fails
  ↓
retry later

The order itself still exists.

Queues also provide a buffer during temporary overload.

Incoming: 20,000 jobs/min
Workers:  10,000 jobs/min

Difference:
10,000 jobs/min temporarily stored in queue

Instead of failing immediately, the system accumulates a backlog and processes it when capacity becomes available.

However, queues require their own reliability mechanisms:

Retry policy
Dead-letter queue
Duplicate handling
Visibility timeout
Idempotent consumers
Queue-depth monitoring

Health Checks

A system cannot route around failures unless it knows which instances are healthy.

Health checks usually answer questions such as:

Is the process alive?
Can it accept traffic?
Are critical dependencies usable?

A load balancer may check:

GET /health

and receive:

{
    "status": "healthy"
}

If one instance fails:

App 1 → healthy
App 2 → unhealthy
App 3 → healthy

the load balancer removes App 2 from traffic.

There are often different kinds of checks.

Liveness asks:

Should this process be restarted?

Readiness asks:

Should this instance receive requests right now?

An application may be alive but not ready while warming caches, applying migrations, or waiting for an important dependency.

For a deeper discussion, see Health Checks, Readiness, and Liveness Probes.

Reliable Data Storage

Reliability is not only about keeping APIs online. Data must also survive failures.

Consider a database with a single disk:

Database
   │
   ▼
Single Disk

If the disk fails and no backup exists, the application may recover while the data does not.

Reliable storage usually combines multiple mechanisms:

Replication
+
Backups
+
Snapshots
+
Transaction logs
+
Recovery testing

Replication protects against some infrastructure failures:

Primary
  │
  ├──► Replica A
  └──► Replica B

But replication is not a replacement for backups.

If application code accidentally executes:

DELETE FROM customers;

replication may quickly copy that deletion to every replica.

A backup provides a separate recovery point.

The practical question is therefore not:

Do backups exist?

but:

Can the system actually restore from them?
How long does recovery take?
How much data could be lost?

These ideas are commonly expressed as:

RPO = acceptable data loss

RTO = acceptable recovery time

For example:

RPO: 5 minutes
RTO: 30 minutes

means the architecture should normally lose no more than about five minutes of data and should recover within about thirty minutes after a qualifying disaster.

Observability and Alerting

A reliability mechanism is useful only if failures can be detected.

Imagine an API returning 500 errors for 20% of requests.

If nobody notices for six hours, recovery mechanisms are incomplete.

Useful reliability signals include:

Request rate
Error rate
Latency
CPU utilization
Memory utilization
Database connections
Queue depth
Retry rate
Circuit breaker state
Replication lag
Cache hit rate
Dependency failures

For example:

Normal:
Error rate = 0.05%

Incident:
Error rate = 12%

An alert should identify that something meaningful changed.

Latency percentiles are also important.

p50 = 60 ms
p95 = 120 ms
p99 = 4,500 ms

The average may look acceptable while a small but important group of users experiences severe delays.

Reliable systems therefore monitor both successful operation and degraded behavior.

Example: Making an Online Store Reliable

Consider a small online store.

Stage 1: Simple architecture.

Users
  │
  ▼
Application
  │
  ▼
Database

It works, but both components are single points of failure.

Stage 2: Add multiple application instances.

                  ┌──► App 1
Users ──► LB ─────┼──► App 2
                  └──► App 3

One application instance can now fail without taking down the entire service.

Stage 3: Add database replication.

Application
    │
    ▼
 Primary DB
    │
    ├──► Replica 1
    └──► Replica 2

Database infrastructure now has redundancy.

Stage 4: Add timeouts.

The product page calls several services:

Inventory
Reviews
Recommendations
Pricing

Each call receives a bounded timeout.

Inventory       500 ms
Pricing         500 ms
Reviews         800 ms
Recommendations 300 ms

A slow recommendation service can no longer block requests indefinitely.

Stage 5: Add graceful degradation.

Recommendations unavailable
      ↓
Product page still works
```

The recommendation section simply disappears temporarily.


Stage 6: Add circuit breakers.


If the review provider starts failing continuously:


Failures increase
     ↓
Circuit opens
     ↓
Stop calling provider temporarily
     ↓
Serve product page without reviews

Stage 7: Move secondary work to queues.


Checkout
   │
   ├──► Save Order
   │
   └──► Queue
          │
          ├──► Confirmation Email
          ├──► Analytics
          ├──► Warehouse Notification
          └──► CRM Synchronization

The CRM being down no longer prevents checkout.


Stage 8: Add idempotency to payments.


Payment request
     │
     ▼
Idempotency key
     │
     ▼
Exactly one logical payment

A network retry cannot accidentally create a second charge.


Stage 9: Add monitoring and automatic recovery.


Health checks
     │
     ├──► remove unhealthy instances
     │
Metrics
     ├──► detect rising errors
     │
Alerts
     └──► notify operators

The final architecture may look like this:


                         ┌──────────────┐
Users ──► Load Balancer ─┤ App Servers  │
                         └──────┬───────┘
                                │
                 ┌──────────────┼───────────────┐
                 │              │               │
                 ▼              ▼               ▼
              Cache        Primary DB       Services
                              │               │
                       ┌──────┴──────┐       │
                       ▼             ▼       │
                   Replica 1     Replica 2   │
                                              │
                                       Timeouts
                                       Retries
                                       Circuit Breakers
                                              │
                                              ▼
                                            Queue
                                              │
                                  ┌───────────┼───────────┐
                                  ▼           ▼           ▼
                               Email       Invoice      CRM
                               Worker      Worker       Worker

No individual technique makes the system reliable.


Reliability comes from layers of protection:


Redundancy
+
Timeouts
+
Controlled retries
+
Circuit breakers
+
Isolation
+
Graceful degradation
+
Durable queues
+
Reliable storage
+
Monitoring
+
Recovery

Common Reliability Mistakes


Calling Services Without Timeouts


A dependency that never responds can consume connections and workers until the caller also becomes unavailable.


Every remote dependency should normally have a bounded waiting time.


Retrying Everything


Retries should not be automatic for every error.


Validation error → retry? No

Authentication error → retry? Usually no

Temporary network failure → retry? Often yes

Rate limit → retry? Maybe, after delay

Payment timeout → retry? Only with idempotency

Retry policy must understand the type of failure.


Keeping Hidden Single Points of Failure


An architecture may have ten application servers but still depend on:


One Redis instance
One database
One queue broker
One NAT gateway
One configuration service

Reliability must be evaluated across the entire request path.


Treating Every Feature as Critical


A recommendation widget should not normally have the same reliability requirements as checkout.


Classifying features helps decide what should fail open, fail closed, retry, degrade, or remain strictly consistent.


Having Backups but Never Testing Restore


A backup that cannot be restored is not a recovery strategy.


Restore procedures should be tested before an actual incident.


Running at 100% Capacity


A system operating permanently near maximum capacity has little room for failures.


Consider:


Normal traffic:
90% CPU

One server fails:
remaining servers must absorb its traffic

Result:
100% CPU
timeouts
retries
larger outage

Reliable systems usually keep some spare capacity so that they can tolerate component failures or sudden load increases.


Production Checklist


  • Identify single points of failure.
  • Run critical services with redundancy.
  • Set explicit timeouts for remote calls.
  • Retry only transient failures.
  • Use exponential backoff and jitter.
  • Make retried side-effecting operations idempotent.
  • Use circuit breakers for repeatedly failing dependencies.
  • Isolate unrelated workloads with bulkheads.
  • Design non-critical features for graceful degradation.
  • Use durable queues for asynchronous work.
  • Monitor queue depth, retries, errors, and latency.
  • Replicate critical data and maintain independent backups.
  • Test restoration and failover procedures.
  • Keep enough capacity to survive expected failures.

Conclusion


Reliability is not the absence of failures.


Reliability is the ability to continue operating when failures happen.


A reliable architecture assumes that servers will crash, networks will become slow, dependencies will fail, and software bugs will eventually reach production.


It then limits the consequences.


Failure happens
      ↓
Detect it
      ↓
Contain it
      ↓
Keep critical functionality working
      ↓
Recover
      ↓
Return to normal

The most reliable systems are usually built from multiple simple protections rather than one perfect mechanism.


Key Takeaway: design software so that the failure of one component becomes a small incident instead of a system-wide outage.






            

Author

Alex Snowgirl

Alex Snowgirl

Nov 09, 2025 3 25
Enjoyed this article?

Support Alex Snowgirl

Buy me a coffee

This helps Alex Snowgirl continue creating useful content

Related articles

Comments (0)