Skip to content

Expert · SRE: reliability and incidents · topic 45 of 53 · 35 min

Resilience patterns, capacity and chaos engineering

Explanation

Parts of a system will fail, so design for it. Put a timeout on every network call, retry only safe requests with exponential backoff and jitter, and use circuit breakers, bulkheads, rate limits and load shedding so one failing part cannot take down everything. Idempotency keys make retried payments safe. Plan capacity for known peaks, and use chaos engineering to test your defences on purpose, starting small.

Failure is normal

In a system of many services, something is always a little broken: a slow database, a network blip, a pod being replaced. Resilience means the system keeps serving users, perhaps with less functionality, when parts fail. The danger is a cascading failure: one slow service makes its callers wait, they run out of threads, their callers wait too, and soon the whole site is down.

Timeouts

A timeout is the longest you will wait for a reply. Without one, a hung dependency keeps your threads or connections busy forever. Every network call needs a timeout. Choose it from the dependency's normal p99 latency plus a margin, and keep it shorter than the timeout of whoever is calling you, or your work is wasted.

Retries with exponential backoff and jitter

Many failures are brief, so one more try often works. But careless retries make outages worse: if a struggling service is hit by every client retrying at once, it never recovers. Rules:

  • Retry only temporary errors (timeouts, 503, 429 Too Many Requests) and only operations that are safe to repeat.
  • Use exponential backoff: wait base x factor to the power of the attempt, with a cap. For base 1 s, factor 2 and cap 30 s: 1, 2, 4, 8, 16, 30 seconds.
  • Add jitter: a random part of the wait, so thousands of clients do not retry at the same moment.
  • Limit attempts (often 3) and retry at one layer only. If three layers each try 3 times, the bottom service can get 27 calls for one click.
python
import random
import time

def call_with_retries(call, attempts=4, base=0.5, factor=2, cap=8):
    for attempt in range(attempts):
        try:
            return call()
        except TimeoutError:
            if attempt == attempts - 1:
                raise
            delay = min(cap, base * factor ** attempt)
            time.sleep(random.uniform(0, delay))   # "full jitter"
Retry with capped exponential backoff and full jitter: wait a random time between 0 and the backoff value.

Idempotency keys

An operation is idempotent if doing it twice has the same effect as doing it once. Reading a balance is idempotent; "debit 500 rupees" is not. If the reply to a payment request is lost and the app retries, the customer could be charged twice. The fix is an idempotency key: the client creates a unique ID for each payment and sends it with every attempt, often in an Idempotency-Key header. The server stores the result for that key and, on a repeat, returns the saved result instead of charging again.

Circuit breakers

A circuit breaker wraps calls to a dependency and stops calling it when it keeps failing. It has three states:

StateBehaviour
Closed (normal)Calls go through. Consecutive failures are counted; at a threshold the breaker opens.
OpenCalls fail at once without touching the dependency, giving it time to recover. After a cool-down it goes half-open.
Half-openOne trial call is allowed. Success closes the breaker; failure opens it again.
Think of it like this: It works like the MCB (miniature circuit breaker) in your home's electrical board. When there is a fault, it trips and cuts the circuit so the wiring does not burn. You fix the problem and switch it back on to test.

Bulkheads, rate limits, load shedding, graceful degradation

  • Bulkheads: separate pools of threads or connections per dependency, like the watertight compartments of a ship. A slow recommendations service can then use up only its own pool, not the threads that payments need.
  • Rate limiting: cap how many requests each client may send, for example 100 per minute per API key, and answer 429 Too Many Requests above that.
  • Load shedding: when the server is overloaded, reject low-priority work early (with 503) so the important work stays fast, instead of everything becoming slow.
  • Graceful degradation: when a dependency fails, serve a reduced version. Show cached prices, hide "people also bought", but keep checkout working.

Capacity planning

Capacity planning means making sure you have enough resources for future demand. Combine normal growth with known peaks: a sale day, an IPL final, tatkal opening at 10 am, the last day for filing tax returns. Load test to find where the system breaks, keep headroom (many teams aim to run below about 60 to 70% of capacity at peak), keep spare capacity so one zone can fail (N+1), and scale up before a known event, because autoscaling takes minutes to react.

Chaos engineering

Chaos engineering is running careful experiments that inject failures on purpose, to learn how the system behaves before a real failure teaches you. Netflix made it popular with Chaos Monkey, which randomly stopped servers.

  1. Define the steady state: a metric that shows normal behaviour, such as payment success rate.
  2. Write a hypothesis: "If one of three payment pods is killed, success stays above 99.5%."
  3. Inject a realistic failure: kill a pod, add 300 ms of network delay, fill a disk.
  4. Keep the blast radius small: start in staging, then a small slice of production, with a clear abort switch.
  5. Compare results with the hypothesis, fix weaknesses, and repeat.

Tools include Chaos Mesh and LitmusChaos (both CNCF projects for Kubernetes), AWS Fault Injection Service, Azure Chaos Studio and Gremlin.

Careful: Common mistakes: (1) No timeouts at all. (2) Retrying immediately with no backoff or jitter, creating a retry storm. (3) Retrying payments or other non-idempotent requests without an idempotency key. (4) Retrying at every layer. (5) Running chaos experiments in production with no hypothesis, no monitoring and no abort plan.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Resilience patterns, capacity and chaos engineering · 1/8

Resilience patterns

In this lesson you will learn design patterns that keep a system running when parts of it fail.

Prefer a video by a teacher? Find free videos on “resilience patterns circuit breaker retry backoff jitter chaos engineering” on YouTube ↗

In real life: Sale day at an online store

An Indian e-commerce company prepares for its biggest sale. Weeks before, it load tests checkout at three times last year's peak and finds the coupon service fails first. The team adds a 300 ms timeout and a circuit breaker around coupons: if coupons fail, checkout continues without the discount and shows a message, instead of failing. Payment calls carry idempotency keys so app retries never charge twice. A chaos experiment in staging kills a payment pod every 10 minutes to confirm the success rate holds. Capacity is scaled up the night before. On sale day the coupon service does fall over for 6 minutes, and customers can still pay.

Practice (0/3 done)

Task 1 · Code · Python

Write backoff_schedule(base, factor, cap, retries) that returns a list of the wait times before each retry: base x factor to the power i for i = 0, 1, 2 ... retries - 1, but never more than cap. No randomness here, so it is easy to test. Example: backoff_schedule(1, 2, 30, 6) returns [1, 2, 4, 8, 16, 30].

Task 2 · Code · Python

Write a CircuitBreaker class. __init__(self, failure_threshold, reset_timeout, clock) where clock is a function returning the current time in seconds. Keep self.state as "closed", "open" or "half_open". allow_request(): in "closed" or "half_open" return True; in "open", if clock() - opened_at >= reset_timeout, switch to "half_open" and return True, otherwise return False. record_success(): reset the failure count and set "closed". record_failure(): in "half_open", open again at once; in "closed", count consecutive failures and open when the count reaches failure_threshold. Opening sets opened_at = clock() and resets the count.

Task 3 · Practice

Plan a first chaos experiment for an "orders" service with 3 pods on Kubernetes that calls a "payments" service. Write the steady state, the hypothesis, the failure you will inject, where you will run it, how you will limit the blast radius, and when you will abort.

Quick quiz

Question 1

1.Why add jitter to retry delays?

Question 2

2.A mobile app retries a payment after its reply was lost. What prevents a double charge?

Question 3

3.A circuit breaker is open. What happens to a new call before the cool-down ends?

Question 4

4.What are bulkheads?

Question 5

5.Which is the best first chaos experiment?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

Why add jitter to retry delays?

Answer: So many clients do not retry at the same moment and overload the service again

Jitter spreads retries out in time and prevents synchronised retry storms.

A mobile app retries a payment after its reply was lost. What prevents a double charge?

Answer: An idempotency key stored by the server

The server sees the same key and returns the saved result instead of charging again.

A circuit breaker is open. What happens to a new call before the cool-down ends?

Answer: It fails immediately without calling the dependency

Failing fast protects both the caller and the struggling dependency.

What are bulkheads?

Answer: Separate resource pools per dependency so one slow dependency cannot use up everything

Like a ship's compartments, they contain the damage to one part.

Which is the best first chaos experiment?

Answer: Kill one pod in staging with a written hypothesis and an abort condition

Start small, in staging, hypothesis-driven, with a way to stop.

With exponential backoff and a 1 second base, how long is the wait after the 3rd failure?Related

Answer: 4 seconds

The waits double: 1, 2, 4. After the 3rd failure the wait is 4 seconds.

From Background jobs and queues

Which request is safe to retry automatically after a network error?Related

Answer: GET /api/notes

Reading notes again changes nothing. A POST may already have worked, so repeating it can create duplicates or double payments.

From Loading, errors and slow networks

Why can retrying a payment be dangerous?Related

Answer: The first attempt may have succeeded, causing a double charge

Only retry idempotent operations, or use idempotency keys.

From Promise.all, allSettled, race and any

When is it safe to retry a request automatically?Related

Answer: For idempotent requests, such as GET or a POST with an idempotency key

Retrying a non-idempotent request can repeat its effect, like charging twice.

From Calling other APIs safely with the HTTP client

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. This is the last topic of “SRE: reliability and incidents”. The module exam comes next.
  2. Module 10 exam: SRE, reliability and incidents · 13 questions · 20 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, DevSecOps. 53 topics left in the course before the final exam.

← Previous: Blameless postmortems and toil