Learn why failure is normal, how redundancy and availability maths work, and how timeouts, retries, idempotency and backups keep a system useful when parts break.
What you'll learn: By the end of this lesson you will be able to find the single points of failure in a design and remove them with redundancy. You will calculate the availability of components wired in series and in parallel. You will write a safe retry policy using exponential backoff with jitter. You will explain why untested backups are not backups.
Think about a restaurant with one chef, one oven and one door. On a quiet night it works fine. The day the chef is ill, the oven trips a fuse, or someone locks the door by mistake, the restaurant is closed. Nothing about the food changed. The design simply had no spare part for anything.
A reliable restaurant keeps a second chef on call, a backup oven, and a side entrance. It also has a rule for bad nights: serve a shorter menu instead of closing. Software systems need the same habits. Machines break, networks drop packets, disks fill up, and deployments go wrong. Reliability is the craft of making sure a failure in one place does not become a failure for every user.
The core idea
Failure is normal, so design for it instead of hoping it will not happen. Every component you use will fail eventually. A disk dies, a data centre loses power, a library has a bug, a neighbouring service gets slow. You cannot prevent all of this. What you can do is limit how far a failure spreads and how long it lasts.
Three words come up constantly, and they mean different things:
Reliability: the system does the right thing, even when parts fail.
Availability: the fraction of time the system is up and answering requests correctly. This is the number you see written as "99.9%".
Fault tolerance: the system keeps working despite a fault. A fault is one component misbehaving. A failure is the whole system failing to serve users. Good design stops faults from turning into failures.
A single point of failure (SPOF) is any one component whose failure takes the whole system down. The hunt for SPOFs is the first reliability exercise on any design. Take your architecture diagram and ask of every box: "If this disappeared right now, what would users see?" If the answer is "everything stops", you have found one.
Typical SPOFs in an online shop:
A single application server.
A single database with no standby.
A single load balancer in front of many healthy servers. This one surprises people: you added many servers for safety, then put one door in front of them.
A single data centre or availability zone (a physically separate location inside one cloud region, which is a geographic area containing several such zones).
A hidden dependency, such as one DNS provider, one certificate that expires on a certain date, or one third-party payment or login service.
NOTE
SPOFs are not only hardware. A configuration file that every service reads, a shared message queue, or one person who knows how to deploy are all single points of failure too.
Redundancy: the basic cure
Redundancy means having more than one of something, so that when one copy fails another takes over. There are two common shapes:
Active-active: all copies serve traffic at the same time. If one dies, the others absorb its share. Application servers behind a load balancer work this way.
Active-passive (standby): one copy serves traffic while another waits, ready to be promoted. Many databases work this way, because only one copy should accept writes at a time.
Read it by deleting one box at a time. Either load balancer can die, either app server can die, and the database has a standby, so users still get an answer. Redundancy has a price: you pay for spare capacity, and you take on new problems such as how the copies stay in sync and which one is in charge (System Design: Replication).
Redundancy only helps when failures are independent. Two servers in one rack share a power supply, and two copies of the same buggy code crash on the same bad request. Spread copies across racks, availability zones and, for the most critical systems, regions.
Availability maths
Availability is a probability, so you can calculate it. Write it as a number between 0 and 1: 99.9% is 0.999.
Components in series multiply. If a request must pass through several components and all must work, the chain's availability is the product of the parts. For illustration, take three components at 99.9% each: 0.999 × 0.999 × 0.999 ≈ 0.997, about 99.7%. Each part alone allows roughly 8.8 hours of downtime a year; the chain allows about 26. Long chains of synchronous calls quietly erode reliability.
Components in parallel improve. With two independent copies where at least one must work, the chance of both being down at once is the product of their failure probabilities: availability = 1 − (failure of A × failure of B). Two servers at 99% each (1% failure) are both down 0.01 × 0.01 = 0.0001 of the time, so the pair is 99.99% available. This holds only if failures are truly independent and failover itself works.
A worked example
For illustration, assume this request path. All figures are assumptions, not measurements.
Component
Copies
Each
Combined
Load balancer pair
2
99.9%
1 − 0.001² = 99.9999%
App servers
3
99%
1 − 0.01³ = 99.9999%
Primary database with standby
2
99.5%
1 − 0.005² = 99.9975%
Third-party login service
1
99.9%
99.9%
The request needs all four tiers, so multiply the combined column. The first three cost about 0.0027% together (0.0001% + 0.0001% + 0.0025%). The login service costs 0.1%. The total is roughly 99.9% (about 99.897% before rounding).
The lesson is in the shape of the result. After you add redundancy, the remaining single dependency dominates. To improve the system further, add a fallback for login, for example letting signed-in users keep browsing, rather than a fourth app server.
WARNING
Parallel maths assumes independence and perfect failover. In real systems the failover mechanism itself can fail, and copies often fail together. Treat the formula as an upper bound, not a promise.
Timeouts: never wait forever
When service A calls service B and B stops responding, A has to decide how long to wait. If it waits forever, A's threads pile up behind the stuck calls, A is exhausted too, and its own callers start waiting. One slow component can freeze a whole chain.
The fix: every network call needs a timeout. Pick it from the latency you expect. If a call normally takes 20 ms and its 99th percentile is 100 ms, a few hundred milliseconds is reasonable and 30 seconds is not. Use separate timeouts for connecting (fast: a machine is either there or it is not) and for reading the response (longer). And respect the caller's own deadline: if the user's browser gives up after 10 seconds, the backend should not keep working for 60.
Retries, backoff and jitter
Many failures are brief: a lost packet, a restarting server, a leader election. Trying again a moment later often works. That is a retry, and it is powerful and dangerous.
Why naive retries make outages worse
Suppose a database starts responding slowly. Clients time out and retry immediately, so the database now receives the original load plus the retries, gets slower, and triggers more retries. This feedback loop is a retry storm, and it can keep a system down long after the original problem is gone.
It compounds across layers. If the app tier makes up to 3 attempts against the service tier, and the service tier makes up to 3 against the database, one user request becomes 3 × 3 = 9 database calls. A third layer makes it 27, and the system hammers the database hardest when it is weakest.
Exponential backoff with jitter
Exponential backoff: wait longer after each failure, for example 100 ms, 200 ms, 400 ms, up to a cap such as 10 seconds. This gives the struggling service room to recover.
Jitter: add randomness to the wait. Without it, a thousand clients that failed together retry together in synchronised waves.
A common recipe is "full jitter": wait a random time between zero and the current backoff ceiling.
import random, time
defcall_with_retry(operation, max_attempts=4, base=0.1, cap=10.0):
for attempt inrange(max_attempts):
try:
return operation()
except TransientError:
if attempt == max_attempts - 1:
raise
ceiling = min(cap, base * (2 ** attempt))
time.sleep(random.uniform(0, ceiling))
The decision of whether to retry at all is a short flowchart. Each question below must say yes before the client tries again.
flowchart TD
E["Call failed"] --> Q1{"Is the error transient?"}
Q1 -->|"no, for example 400 or 403"| F1["Fail fast, do not retry"]
Q1 -->|yes| Q2{"Idempotent or carries a key?"}
Q2 -->|no| F2["Do not retry, surface the error"]
Q2 -->|yes| Q3{"Attempts and retry budget left?"}
Q3 -->|no| F3["Give up and degrade"]
Q3 -->|yes| W["Wait with backoff and jitter, then retry"]
Read it top to bottom: most failures leave through the left-hand exits. Only transient errors on safe operations, within budget, earn another attempt. In practice:
Limit attempts. Two or three retries is typical.
Retry at one layer, usually the outermost one that can do it safely.
Consider a retry budget, for example "retries may be at most 10% of normal traffic", so a failing dependency cannot be flooded.
Idempotency: making retries safe
A retry raises an awkward question. The first attempt timed out, but did it run? Maybe the server did the work and only the reply was lost. Retrying "charge the customer" might do it twice.
An operation is idempotent if doing it several times has the same effect as doing it once. Setting a profile name to "Asha" is idempotent. Adding 1 to a counter is not. For operations that are not naturally idempotent, use an idempotency key: the client generates a unique ID for the intent and sends it with every attempt. The server remembers keys it has processed and returns the original result instead of repeating the work. Our API design lesson shows this over HTTP.
sequenceDiagram
participant C as Client
participant S as Server
participant D as Database
C->>S: POST /payments (key A81F)
S->>D: charge and record key A81F
D-->>S: ok
S--xC: response lost
Note over C: timeout, wait with backoff
C->>S: POST /payments (key A81F)
S->>D: key A81F already seen?
D-->>S: yes, return saved result
S-->>C: 201 Created (same payment)
The key is what turns the second attempt into a lookup. Without it, the second request is a second charge.
Health checks and graceful degradation
A health check is a small endpoint, such as /healthz, that load balancers and orchestrators call regularly. A server that fails several checks in a row is taken out of rotation, and a platform like Kubernetes can restart it. Distinguish two kinds:
Liveness: "is the process running at all?" A failure means restart it.
Readiness: "is it ready to take traffic?" A failure means stop sending requests but leave it alone, for example while it warms its cache.
Keep health checks cheap and honest. A check that always says "ok" is useless, and a check that queries five downstream services can turn one outage into a total outage when every server marks itself unhealthy at once.
When a non-essential dependency fails, the system should do less, not nothing. This is graceful degradation, the "shorter menu" from our restaurant.
If this fails
Degrade by
Essential path stays up
Recommendation service
Show best sellers instead
Browsing
Image resizing workers
Serve the original image
Viewing
Search index
Disable the search box, keep category browsing
Browsing and checkout
Review counts
Show a cached count or hide it
Everything else
Decide in advance which features are essential and make the code able to switch the others off. A circuit breaker (a component that stops calling a failing service for a while) is a common tool; Advanced: Resilience Patterns covers it.
Backups and restore testing
Redundancy protects you from hardware dying. It does not protect you from a bad deployment that corrupts data, an engineer who runs the wrong delete command, or ransomware, because replication faithfully copies the damage to every replica. For that you need backups: separate, point-in-time copies kept where the damage cannot reach.
Keep copies in a different failure domain (a set of things that can fail together, such as one account, zone or region), ideally a different account and region, with restricted delete permissions.
Know your targets. The recovery point objective (RPO) is how much data you can afford to lose, measured in time. The recovery time objective (RTO) is how long you can afford to be down. Backup frequency follows RPO; restore speed follows RTO.
Test restores. A backup you have never restored is a hope, not a backup. Schedule drills, time them, and check the restored data is usable.
IMPORTANT
The most common backup failure is discovering, in the middle of an incident, that the backups were empty, incomplete, encrypted with a lost key, or too slow to restore. Practise before you need it.
In practice: a checkout that depends on a payment gateway
NOTE
A realistic scenario, not a case history. This is a composite example built from common production patterns. It does not describe any specific company, and every number in it is an illustrative assumption.
The situation
An online retailer with eight engineers and one on-call rotation runs checkout as a Java service on Kubernetes, backed by PostgreSQL and Redis (for carts). Card payments go through a third-party payment gateway over HTTPS. Assume 40 orders per minute on a normal day and 300 on promotion days, a gateway that normally answers in about 1 second at p99, and a contract promising 99.9% availability.
The pain, all assumed: when the gateway slows down, the checkout success rate falls from 99% to about 70%, the checkout pods run out of threads within minutes, and support sees roughly 0.3% of orders charged twice. The defaults explain it. The gateway client has a 30 second timeout and 3 retries, and the ingress (Nginx) has been configured to retry failed requests once, even POSTs (Nginx does not retry POSTs by default). Nothing carries an idempotency key.
Starting architecture
One request can become 2 × 4 = 8 gateway calls, each holding a thread for up to 30 seconds, and none of them is safe to repeat.
Options considered
Option
What it fixes
What it costs
Risk
A. Smaller change: 3 s timeout, 1 retry, remove the ingress retry
Thread exhaustion and retry multiplication
An afternoon of config
Duplicate charges remain; slow-but-successful payments are reported as failures
B. Timeouts, jittered retries, idempotency keys, circuit breaker and a "payment unknown" order state with reconciliation
Duplicates, pile-ups and the unknown outcome
Two to three weeks; new states to test
More paths in checkout; the reconciliation job needs an owner
C. B plus a second payment provider with automatic failover
Gateway outages
New contract, second integration, doubled compliance scope
Failover can double-charge if both providers are tried; rarely exercised code
The decision
Context
Duplicate charges and thread exhaustion both trace to unsafe retries against one dependency.
Decision
Option B, with A's timeout values shipped first as a quick win. Defer C.
Consequences, good
A payment can be retried safely; a slow gateway no longer freezes the whole service.
Consequences, bad
Orders can sit in payment_unknown for minutes; customer support needs a script for them.
Revisit when
Gateway downtime exceeds the contract for two quarters, or lost revenue from outages exceeds the yearly cost of a second provider.
Rollout and verification
The new order lifecycle makes the uncertain case explicit instead of guessing:
stateDiagram-v2
[*] --> Created
Created --> PaymentPending: call gateway with key
PaymentPending --> Paid: success
PaymentPending --> Failed: declined or refused by open breaker
PaymentPending --> PaymentUnknown: call timed out
PaymentUnknown --> Paid: reconcile finds charge
PaymentUnknown --> Failed: reconcile finds none
Ship it behind a feature flag in four steps:
Rollout behind a feature flag in four stepsShadow mode to Canary; Canary to 25% of checkouts; 25% of checkouts to 100%Shadow mode: decisions loggedShadow modedecisions loggedCanary: 5% of checkoutsCanary5% of checkouts25% of checkouts: full promo day25% ofcheckoutsfull promo day100%: old path removed100%old path removed
Prove it with dashboards, not hope: checkout success rate, gateway call p99, circuit breaker state, the count and age of payment_unknown orders, retries as a share of traffic, and a duplicate-charge report built from gateway settlement files. The target is no duplicate charges in the canary and payment_unknown older than 15 minutes at zero. Before full rollout, run a game day that makes the gateway slow in staging.
The 2 a.m. failure
The page reads "Gateway p99 above 5 s and breaker open for 5 minutes". The gateway is degraded, so charge calls time out; each timed-out call may or may not have charged the card, so those orders land in payment_unknown. Once the breaker is open, new attempts are refused before any call is sent, so their outcome is known: nothing was charged.
sequenceDiagram
participant U as Customer
participant CO as Checkout
participant BR as Circuit breaker
participant GW as Gateway
U->>CO: place order
CO->>BR: charge with key K1
BR--xGW: call times out
BR-->>CO: outcome unknown
CO->>CO: mark payment_unknown
CO-->>U: order received, confirming payment
Note over CO,GW: reconcile job polls gateway by key K1 every minute
First three checks: the gateway status page, kubectl get pods -n checkout (are pods healthy, or only the dependency?), and SELECT status, count(*) FROM orders WHERE created_at > now() - interval '30 minutes' GROUP BY status. Mitigation: leave the breaker open, show "confirming your payment" for orders already in payment_unknown and "payment temporarily unavailable, please retry" for refused attempts, pause promotional emails, and let the reconciliation job settle the unknown orders when the gateway returns. Do not disable retries by hand, and do not mark orders failed to clear the queue. Follow-up: give the payment client its own thread pool (a bulkhead) so a slow gateway cannot starve cart requests, and add an alert on the age of the oldest payment_unknown order.
Architect's notes
Measure before tuning. Take timeouts from the gateway's observed p99 and p99.9 over several weeks, not from vendor defaults.
Ask in the review: "What happens to the order if the response is lost after the charge succeeded?" If nobody can answer, the design is missing a state.
Count total attempts across layers. Client, ingress, service and SDK retries multiply. Write the product on the design page.
Idempotency needs an owner. Key storage, expiry and the reconciliation job are real operational work; name the team that gets paged.
Redundancy is not free. A second provider doubles integration and compliance surface. Buy it only when outage cost is measured and exceeds it.
Defer multi-provider failover until the cheaper fixes are in and you have the downtime numbers.
Common mistakes
Adding servers but keeping one load balancer or database. The single remaining box is still a SPOF. Re-draw the diagram and look for the thin neck.
Calling dependencies without timeouts. One slow service then stalls every caller. Set a connect and a read timeout on every outbound call.
Retrying forever or at every layer. This multiplies load exactly when the backend is weakest. Cap attempts, use backoff with jitter, and retry in one place.
Retrying non-idempotent operations blindly. Customers get charged twice. Use idempotency keys or make the operation naturally idempotent.
Treating replication as a backup. Replicas copy mistakes within moments. Keep separate, versioned backups in another failure domain.
Never testing restores or failover. Untested recovery fails when it matters. Run scheduled drills and measure the time.
Health checks that test too much. A check that depends on every downstream service can take down the whole fleet at once. Keep it shallow and separate liveness from readiness.
Key takeaways
Failure is normal. Design so a fault in one component does not become a failure for users.
Hunt single points of failure. Ask of every box in the diagram what happens if it disappears, including hidden ones like DNS and certificates.
Redundancy needs independence. Copies in the same rack, zone or codebase tend to fail together.
Series multiplies, parallel improves. A chain is weaker than its weakest link; independent copies make a tier far more available.
Timeouts on every call. Waiting forever turns one slow service into a system-wide stall.
Retry politely. Bounded attempts, exponential backoff and jitter, and only for transient errors.
Idempotency makes retries safe. Use idempotency keys for operations that must not run twice.
Backups are for mistakes, and only tested restores count. Replication does not protect you from deletions or corruption.
Practice exercises
Exercise 1 — Starter (30 min): Draw a simple architecture for a blog: browser, one web server, one database, one DNS provider. List every single point of failure you can find, including at least two that are not hardware. Then redraw the diagram with the cheapest changes that remove the biggest risks.
Exercise 2 — Intermediate (2 hours): For illustration, assume a request passes through a CDN (99.95%), a load balancer pair (each 99.9%), three app servers (each 99.5%, any one is enough), a primary and standby database (each 99.5%), and a payments provider (99.9%). Calculate the availability of each tier and of the whole path, then convert the result into downtime per year. Identify the weakest tier and propose one change, with its cost, that improves the total the most.
Exercise 3 — Advanced (half day): Write a small simulation (any language) of a service that handles 100 requests per second and fails for 30 seconds. Compare three client behaviours: no retries, immediate retries up to 3 times, and exponential backoff with full jitter. Plot or tabulate the load the server sees over time and the time it takes to recover after the fault ends. Write half a page explaining the differences you observe.
Lesson 11 of 12 · System Design Foundations: From Zero to Your First Design
Learn why failure is normal, how redundancy and availability maths work, and how timeouts, retries, idempotency and backups keep a system useful when parts break.
In the rest of this lesson
Timeouts: never wait forever
Retries, backoff and jitter
Idempotency: making retries safe
Health checks and graceful degradation
Backups and restore testing
In practice: a checkout that depends on a payment gateway
Enrolling is free. It unlocks every lesson in System Design Foundations: From Zero to Your First Design, tracks your progress across devices and keeps your quiz scores. The newsletter is optional.
Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.
Get new articles by email
Free. We email a confirmation link first, and you can unsubscribe from any email.