SwadhyAI Notes

All System Design notes

System Design Reliability

Timeouts, Retries & Circuit Breakers

The on-call page fires: Uber matching is slow. Maps, payments, and drivers all sit on the path, and one of them is sick. Without , every waiting thread piles up, clients hammer retries, and a small glitch becomes a city-wide outage.

The network is not reliable. Design for that: set a , retry only safe work with , and open a before a sick dependency pulls everything down.

Ride-request decision

1

Never wait forever on a remote call.

2

Retry only safe, useful attempts.

3

Fail fast when the dependency is sick.

4

Serve cached or degraded data before the user gives up.

Try it: walk a circuit breaker through its states

Each ride request calls a maps service with a timeout. Repeated timeouts open the breaker so threads fail fast and return a cached route. Recovery uses half-open probes before closing again.

Total calls
0
Timeouts
0
Fast failures
0
Fallback routes
0

Circuit breaker

closed

Closed · 0/3 consecutive failures before open.

Call the maps dependency. After three failures the breaker opens and callers fail fast with a cached route instead of waiting on a sick service.

Timeouts free capacity. Without a timeout, every waiting thread holds memory and sockets while the dependency stalls. The timeout counter shows how many calls would have hung in a naive client.

Breakers protect the fleet. Opening the circuit shifts work to fallbacks and stops retry storms from amplifying a brownout into a cascading failure across Uber-scale matching.

What actually happens

Every remote call can drop, hang, or return late. A aborts the wait so threads and connections free up. A shared makes sure retries do not finish after the user already left.

When a failure looks transient, a bounded with and can help, but only if the write is or protected by an . Nested retries without a multiply traffic and create a .

When failure rate stays high, a opens and callers use a . keep payment threads separate from recommendations so one sick leaf cannot exhaust the whole pool.

How to choose timeouts and retries

1. Is every attempt still useful? If the user deadline is gone, do not retry: return or a clear error.

2. Is the operation safe to repeat? Idempotent reads yes; non-idempotent writes only with an . Validation and auth errors never retry.

3. Who owns retries? One layer should retry, not edge, service, and DB client all at once, and cap extras with a .

Simple rule of thumb

Strict timeouts on every call. Bounded retries with backoff and jitter on safe operations. Wrap sick dependencies in a and isolate capacity with .

Fail fast and serve degraded data before aggressive retries melt the recovering service.

Timeouts: knowing when to give up

A is a strict time limit on a network request. If the response doesn't arrive before the timer runs out, the client aborts. Never make a network call without a timeout.

Why timeouts are needed

Without timeouts, a client could wait forever for a dead server. Waiting forever keeps a thread or connection open. Eventually you run out of threads (thread pool exhaustion) and your whole service crashes because an external API was slow.

Connection timeout

Connection timeout bounds how long establishing a connection may take. Depending on the client library, DNS lookup and TLS negotiation may have separate limits, so inspect what the setting actually covers. A warm pooled connection can make normal traffic look fast while new connections during failover time out. Test the reconnect path rather than deriving the timeout only from steady-state requests.

Sensible: 1-5 seconds

Read/request timeout

A read or response timeout limits waiting after connection establishment, but some libraries reset it for each incoming chunk. That can allow an extremely slow stream to continue far longer than expected. Keep an end-to-end deadline in addition to per-stage limits. Cancelling the client wait does not prove the server stopped processing or that a write failed.

Sensible: P99 latency + buffer (e.g., 3-10s)

How to decide timeout duration

  • Look at the P99 latency of the service you are calling. If 99% of requests finish in 200ms, a sensible timeout might be 500ms or 1 second.
  • Consider the user experience. Users won't wait 30 seconds for a web page. Fail fast (e.g., 3 seconds) and show an error or fallback UI.
  • Chain of timeouts: If Service A calls B (timeout 5s), and B calls C, B's timeout to C must be less than 5s, otherwise A will timeout before B finishes.

Reliability budget for a single call

Before writing retry code, decide how much time the user can spare. Then divide that time across connection, server work, one or two retries, and a fallback. A retry that finishes after the caller has already timed out is wasted load.

BudgetGuard / fallbackRetry

Deadline

An end-to-end cap across all attempts. Pass it downstream so services stop work when the user no longer waits.

Retry budget

Limit retries to a small percentage of original traffic, such as 10-20%, to avoid doubling load during brownouts.

Fallback

Return cached data, partial data, or a clear degraded response before the user experience crosses its pain threshold.

Deadline propagation worked example

A is one shared timer for the whole user request. Each service spends from the same clock, so downstream services stop work when there is no useful time left.

mermaid
sequenceDiagram
    participant U as User
    participant E as Edge
    participant A as Service A
    participant B as Service B
    participant C as Service C

    U->>E: Request budget 900 ms
    E->>A: 800 ms left, reserve 100 ms fallback
    A->>B: 300 ms sub-deadline
    A->>C: 250 ms sub-deadline
    B-->>A: Slow response
    A-->>E: Skip retry if remaining < 150 ms
    E-->>U: Cached or partial response before deadline

User budget

900 ms total; the edge keeps 100 ms for fallback and response writing.

Service budgets

Service A allocates 300 ms to Service B and 250 ms to Service C.

Retry rule

If less than 150 ms remains, do not retry; return fallback or partial data.

Retries and idempotency

When a request fails due to a temporary network blip (a transient failure), the simplest fix is to try again. But retries are dangerous if the API is not .

The danger of retrying

Imagine a payment API: POST /charge with $10.

  1. Client sends request to Server.
  2. Server charges the credit card $10.
  3. Server tries to send "Success" back to Client.
  4. Network drops the response packet!
  5. Client times out. Thinks it failed.
  6. Client retries. Server charges another $10.

The solution: idempotency keys

Idempotency means repeating the same operation has the same intended effect as applying it once; it does not require every response to be identical. Repeating a delete may return “not found” after the first successful deletion while the resource still ends in the same state. For a payment POST, the application can provide idempotency using a stable key and stored outcome, even though POST does not promise it by default.

To safely retry writes, clients generate a unique (UUID) and send it in the header. The server saves this key. If the server sees the same key again, it just returns the cached success response instead of charging the card twice.

Retry decision matrix

Retry only when the request is safe, still useful, and unlikely to overload the weak service. Many failures should fail fast or use a fallback.

SituationRetry?Reason
Idempotent read times outYes, boundedSafe if deadline, backoff, jitter, and retry budget exist.
Write with idempotency keyYes, carefullyServer can deduplicate repeated attempts.
Validation or authorization errorNoSame request will fail again; retries waste capacity.
Non-idempotent write without keyNoCan duplicate payments, orders, messages, or side effects.
Deadline already expiredNoUser no longer benefits; extra work harms recovery.
Dependency saturatedOnly with guidanceRespect backoff, Retry-After, circuit breaker, and load shedding.

Retry storms: when good intentions melt systems

A retry storm happens when many clients interpret slowness as failure and all try again. The original problem may be small, but retries add enough extra work to prevent recovery.

mermaid
sequenceDiagram
    participant C as 100k clients
    participant API as API tier
    participant DB as Slow database

    C->>API: Original requests
    API->>DB: Query load
    DB-->>API: Slow responses
    API-->>C: Timeouts
    C->>API: Retry immediately
    C->>API: User refresh / SDK retry
    API->>DB: 2-4x more queries
    DB-->>API: Saturation worsens
    API-->>C: More timeouts

Bad pattern

  • Every layer retries three times independently.
  • No jitter, no global deadline, no retry budget.
  • Retries on non-idempotent writes.

Resilient pattern

  • One owner retries at the edge or caller, not every hop.
  • Full jitter, max attempts, and a shared request deadline.
  • Circuit breaker opens and callers serve cached or degraded data.

Retry amplification math

Retries multiply across layers. If the edge, service, and database client each try three times, one user request can become 27 downstream attempts during the exact moment the dependency is weakest.

InputAmplifyDanger
PolicyAttempts per original requestAt 1M req/minSafer control
No retry1x1M attempts/minUse for non-idempotent writes unless protected by keys.
One owner retries twice2x2M attempts/minBounded retry budget, jitter, shared deadline.
Three layers retry 3x27x27M attempts/minDisable inner retries; open breaker and return fallback.

Central retry ownership

With 1M req/min, two caller retries and three downstream retries can create 6M attempts/min or worse depending on layering. If one layer owns retries and caps them to 10% of live traffic, retry load stays near 100K extra req/min instead of millions.

Retry budget example

If original traffic is 50K req/s and retry budget is 10%, allow at most 5K retry req/s. Once spent, fail fast or use cached responses.

How to say it

"I will centralize retry ownership, pass an end-to-end deadline, add jitter, and cap retries as a percentage of live traffic."

Exponential backoff and jitter

If a server is struggling, immediately retrying hammers it harder. Wait before retrying, and add randomness so all clients don't retry at the exact same time. Suppose 10,000 clients all wait exactly one second after a timeout. They will produce another synchronized burst one second later. Jitter spreads those retry times across a range, reducing that alignment, while a retry budget limits the total extra work. Backoff without a total deadline can still keep obsolete work alive long after the user has given up.

Exponential backoff

Instead of retrying every 1 second, double the wait after each failure: 1s, 2s, 4s, 8s, 16s. This gives the struggling server time to recover.

Jitter (randomness)

If a server drops 1,000 connections at once, and all 1,000 clients use the exact same backoff formula, they will all retry together: a thundering herd. adds random noise to spread the spikes into a smoother curve.

javascript
// Standard Exponential Backoff with Jitter
const MAX_RETRIES = 5;
const BASE_DELAY_MS = 1000;
const MAX_DELAY_MS = 30000;

async function fetchWithBackoff(url, attempt = 0) {
    try {
        return await fetchWithTimeout(url); // Assumes strict timeout
    } catch (error) {
        if (attempt >= MAX_RETRIES) throw new Error("Max retries reached");

        // Exponential backoff: 1s, 2s, 4s, 8s...
        const backoff = BASE_DELAY_MS * Math.pow(2, attempt);
        
        // Cap the maximum wait time
        const cappedBackoff = Math.min(backoff, MAX_DELAY_MS);
        
        // Add Full Jitter: random number between 0 and cappedBackoff
        const sleepTime = Math.random() * cappedBackoff;

        console.log(`Attempt ${attempt} failed. Retrying in ${Math.round(sleepTime)}ms`);
        await new Promise(res => setTimeout(res, sleepTime));
        
        return fetchWithBackoff(url, attempt + 1);
    }
}

Cascading failures

A happens when a problem in one small part of the system spreads to healthy parts, eventually bringing the entire architecture down.

How retries cause cascading failures

Service A

Service B

Database

HealthyWarningOverloaded

The scenario:

  1. The Database gets slightly overloaded and queries slow down from 10ms to 500ms.
  2. Service A is waiting on the DB. Because queries are slow, Service A's threads are stuck waiting.
  3. Users experience lag, so they hit "Refresh" (manual retry).
  4. Service A's automated code also retries failed DB queries.
  5. The Database is now getting 3x the traffic while it is already struggling. The DB completely crashes.
  6. Now Service A's thread pool is completely exhausted waiting for timeouts. Service A crashes.
  7. Service B, which shares the DB, also crashes. The API Gateway starts timing out. Total system outage.

Retries and timeouts alone do not fix this. Aggressive retries create a thundering herd that makes recovery impossible.

Bulkheads and capacity isolation

keep failure contained. Give risky dependencies separate thread pools, connection pools, queues, and rate limits so they cannot steal capacity from the critical path.

Payment pool

Small, protected, strict timeouts, high alert priority.

Recommendations pool

Larger but shedable; fallback to popular items if saturated.

Analytics pool

Async queue, lossy during incidents, never blocks user response.

Circuit breakers stop bad calls; bulkheads stop a bad dependency from consuming the good dependency's resources.

Circuit breakers: stopping the cascade

Inspired by electrical breakers, this pattern monitors external calls. If a service is failing repeatedly, the breaker trips and immediately blocks future calls, giving the dependency time to recover without overwhelming it. A breaker observes recent failures and changes behavior for calls to one dependency. It helps only when rejected calls release resources or use a cheaper fallback. An application that immediately retries elsewhere at the same total volume may simply move overload around. Keep breaker scope, minimum sample size, failure classification, and recovery probes explicit so a small noisy sample does not disable a healthy service.

mermaid
stateDiagram-v2
    [*] --> CLOSED
    
    CLOSED --> OPEN : Failure rate > Threshold (e.g. 50%)
    
    OPEN --> HALF_OPEN : Timeout expires (e.g. 30s)
    OPEN --> OPEN : Fast fail incoming requests
    
    HALF_OPEN --> CLOSED : Success (Service recovered!)
    HALF_OPEN --> OPEN : Failure (Still broken)

The three states

CLOSED (Normal)

Traffic flows freely. Breaker counts failures/timeouts. If failure rate stays below threshold, nothing happens.

OPEN (Tripped)

Failure threshold reached. All requests fail instantly without calling the external service. Protects the struggling service and frees up threads.

HALF-OPEN (Testing)

In the half-open state, admit a small bounded number of trial calls after a cooldown. Successful trials provide evidence that normal traffic can resume; failures return the breaker to open. Recovery should be gradual because one successful lightweight probe does not prove the dependency can handle the full backlog. Coordinate probe volume across replicas so thousands of callers do not all test recovery simultaneously.

Why circuit breakers help

  • Fail fast: Instead of waiting 5 seconds for a timeout when we know the DB is down, return an error in 1ms.
  • Fallback UI: Fail fast so the UI can show cached data or a friendly "Service Degraded" message without hanging.
  • Self-healing: The struggling service gets zero traffic while OPEN. HALF-OPEN safely probes readiness.

Code conceptualization

javascript
class CircuitBreaker {
    constructor(failureThreshold, resetTimeout) {
        this.state = 'CLOSED';
        this.failures = 0;
        this.threshold = failureThreshold;
        this.timeout = resetTimeout;
        this.nextAttempt = null;
    }

    async call(apiFunction) {
        if (this.state === 'OPEN') {
            if (Date.now() > this.nextAttempt) {
                this.state = 'HALF_OPEN';
            } else {
                throw new Error("Circuit is OPEN. Fast failing!");
            }
        }

        try {
            const result = await apiFunction();
            this.reset(); // Success!
            return result;
        } catch (error) {
            this.recordFailure();
            throw error;
        }
    }

    recordFailure() {
        this.failures += 1;
        if (this.failures >= this.threshold) {
            this.state = 'OPEN';
            this.nextAttempt = Date.now() + this.timeout;
        }
    }

    reset() {
        this.failures = 0;
        this.state = 'CLOSED';
    }
}
Breaker stateTraffic behaviorFallback and recovery
ClosedNormal calls flow to dependency.Track latency, failures, saturation, and error budget burn.
OpenFast fail; do not call weak dependency.Serve cached, reduced, queued, or clear error response.
Half-openAllow limited probes with strict concurrency.Close only after healthy samples; otherwise reopen.
RecoveryRamp traffic gradually.Avoid thundering herd by keeping jitter and retry budget active.

How to answer: downstream is down or slow

If asked what happens when a downstream service goes down or gets slow: all network calls need strict timeouts so you don't exhaust the thread pool. For transient errors, use retries with exponential backoff and jitter. To prevent aggressive retries from causing a thundering herd and cascading failure, wrap calls in a circuit breaker. When the failure rate exceeds a threshold, the circuit opens, you fail fast, and you serve degraded or cached data until recovery.

What must never become false

Correctness invariant

Every outbound call has a / . Retries have one owner and a finite so multi-layer retries cannot amplify without bound. Charges, authz, and irreversible writes fail closed, never invent success in a .

Failure timeline: nested retries

t=0      Payment leaf brownout; baseline 1M user req/min
t=10s    Edge retries ×2, service ×3, DB client ×3 → up to ~18M attempts/min
t=20s    Thread pools fill; healthy neighbors time out (cascading failure)
t=30s    Circuit opens; fail-fast + cached/degraded path; amplification stops
t=40s    Half-open probes capped; dependency recovers without stampede
Outcome  Retries without budgets turn a leaf outage into a platform outage

Exercises

  1. Gateway, app, and SDK each retry twice on the same charge API. Sketch amplification and redesign so idempotency keys plus one retry owner keep the invariant.
  2. 2_000 RPS with a 5s hang and no timeout. Estimate in-flight waits. Choose timeout + bulkhead sizes that keep 70% of threads for non-payment work.
  3. Pick fail-closed vs cached fallback for: authorize payment, show recommendations, send receipt email. Name the metric that pages when the breaker stays OPEN.

References

  • Michael T. Nygard: Release It! (timeouts, bulkheads, circuit breakers).
  • Google SRE Book: Cascading Failures / Handling Overload (retry amplification).
  • AWS Architecture Blog / Builders Library: Timeouts, retries, and backoff with jitter.

Going deeper: retry ownership and adaptive concurrency

One retry owner

Decide whether the client, gateway, or service retries. Nested retries create traffic amplification. Publish a token-bucket retry budget per dependency.

Hedging and adaptive concurrency

A hedged request sends a second attempt before the first has finished, usually after a delay chosen from observed latency. The first acceptable response wins and the other attempt is cancelled when possible. This can reduce tail latency for safe reads, but spends additional capacity and can worsen overload. Combine a strict hedge budget with idempotency and load-aware suppression; cancellation does not guarantee already-started backend work disappears.

Practice and further reading

Use these exercises to connect the lesson to a design decision. State your assumptions, trace what happens to one request, and explain the rule that must remain true even during a failure. That rule is the design's correctness invariant.

Focus for this lesson: Compose deadlines, retry budgets, idempotency, jitter, circuit breakers, bulkheads, and adaptive concurrency without multiplying load.

Explain the decision

Which layer should retry?

Read the reasoning

Prefer one layer with full operation context and remaining deadline; multiple retrying layers multiply attempts and hide ownership.

Apply it to a production scenario

Five layers each retry twice. How many attempts can one user request trigger?

Read one approach

Up to 3^5 = 243 attempts. Collapse retry ownership, cap attempts, add jitter, and spend a shared retry budget.

Work through the numbers

A 2% base failure rate with one independent retry leaves what approximate failure probability?

Read the worked calculation

0.02² = 0.04%, but independence often fails during overload, so capacity/retry budgets still matter.

Reason through a failure

What must a circuit breaker never imply?

Read the recovery reasoning

An open circuit does not prove the dependency is down forever; probes are bounded, state is observable, and fallback cannot violate correctness.