← Better Programmer course
04
Intermediate
3 hours 15 minutes

Reliability and Asynchronous Work

Learn each failure-control mechanism on its own, then combine them into workflows that stay safe under delay, duplication, and dependency failure.

Learning outcome

You will be able to model failures, set safe deadlines and retries, design idempotent writes, use queues honestly, and build a complete reliability plan for one user journey.

Jump to a chapter
  1. 1. Failure Models: Name What Can Go Wrong
  2. 2. Timeouts: Stop Waiting Forever
  3. 3. Retries and Exponential Backoff
  4. 4. Idempotency: Make Repeated Requests Safe
  5. 5. Circuit Breakers: Stop Calling a Failing Dependency
  6. 6. Graceful Degradation: Preserve the Core Journey
  7. 7. Queues and Workers: Move Work Out of the Request
  8. 8. Combine the Tools into a Reliable Async Workflow
  9. Applied exercise
  10. Knowledge check

Chapter 1

Failure Models: Name What Can Go Wrong

Reliability starts by describing failures clearly instead of saying that the system may be unstable.

A failure model is a list of ways a component can stop meeting its promise. A process can crash, a machine can disappear, a network can delay or drop messages, a dependency can return bad data, and a person can deploy the wrong configuration. These failures have different causes and need different defenses.

Start with one user journey and inspect every step. For each step, record the expected result, how long it may take, whether it changes data, and what the caller sees when it fails. Also identify the failure domain: one process, one machine, one availability zone, one region, or the whole organization.

For a checkout flow, the catalog may be optional while payment and order storage are critical. If recommendations fail, the page can continue. If payment is uncertain, the system must stop and reconcile rather than guess. This simple classification guides later choices about timeouts, retries, and degraded modes.

Key insight

You cannot design a useful recovery until you can state the failure, its scope, and its user impact.

How to apply this

  • Map failures against a real user journey, not an abstract component list.
  • Separate critical dependencies from optional enhancements.
  • Record how each failure will be detected and who owns recovery.
Scenario — Recommendations block checkout

Situation

The product page waits forever for a recommendation service even though product and price data are healthy.

Decision

Mark recommendations as optional, give them a short deadline, and render the core product page when that deadline is reached.

Chapter 2

Timeouts: Stop Waiting Forever

A timeout limits how long one operation may consume a user or system budget.

A timeout is a deadline after which the caller stops waiting. Without one, a slow dependency can hold threads, connections, memory, and user attention until the whole service becomes saturated. A timeout does not prove that remote work stopped; it only means the caller no longer waits for the result.

Choose deadlines from the end-to-end latency budget. If a page must finish in two seconds, a secondary API cannot use a five-second timeout. Connect, read, and total request timeouts may need different values. The caller must also decide what to show next: an error, cached data, a partial response, or a pending state.

Suppose an address lookup normally takes 100 ms and its p99 is 400 ms. A 700 ms timeout leaves room for normal variation without blocking checkout for several seconds. Metrics should count deadline failures so the team can tell whether the value is too strict or the dependency is unhealthy.

Key insight

A timeout is a resource and experience boundary, not a complete recovery strategy.

How to apply this

  • Set deadlines explicitly at network and job boundaries.
  • Make child deadlines shorter than the remaining parent request budget.
  • Define the user-visible fallback before enabling a timeout.
Scenario — An export request never returns

Situation

A report service waits on a database connection with no deadline, so browser requests pile up.

Decision

Set request and database timeouts, move the long export to a job, and show a clear pending status.

Chapter 3

Retries and Exponential Backoff

A retry repeats work after a failure that may be temporary.

Retries help with transient failures such as a brief network interruption or a busy service. They do not fix permanent failures such as invalid input or missing permissions. Retrying every error wastes capacity and can turn one unhealthy service into a system-wide incident.

Exponential backoff increases the wait between attempts, for example 100 ms, 200 ms, then 400 ms. Jitter adds randomness so thousands of clients do not retry at the same instant. Always set a maximum attempt count or total retry budget, and retry at one deliberate layer rather than at every layer.

A read-only product lookup may safely retry a timeout once. A charge request is different because the first attempt may have succeeded even when its response was lost. That write needs an idempotency design before automatic retries are safe.

Key insight

Retry only failures likely to recover, within a small budget, and only when repeating the operation is safe.

How to apply this

  • Classify errors as retryable or non-retryable.
  • Use capped exponential backoff with jitter.
  • Measure retry volume and stop retries from multiplying across layers.
Scenario — Retry storm after an outage

Situation

Every application instance retries a failing dependency immediately three times, tripling load when it is weakest.

Decision

Retry only transient errors with backoff and jitter, cap attempts, and coordinate with a circuit breaker.

Chapter 4

Idempotency: Make Repeated Requests Safe

Idempotency means repeating the same logical operation has the same intended effect as doing it once.

Idempotency matters because networks cannot always tell a caller whether a request completed. A timeout may happen before processing, during processing, or after the server committed the change but before the response arrived. The caller needs a safe way to try again without creating a second payment or order.

A common design uses an idempotency key generated for one user intent. The server stores the key with the result in the same reliable boundary as the side effect. A repeated request with the same key returns the stored result. A unique database constraint can provide the final protection against races.

When a user presses Pay, the client creates one key for that payment attempt. Network retries reuse it. A new payment intent gets a new key. The server validates that a reused key has the same request details so the key cannot hide conflicting operations.

Key insight

Idempotency converts an uncertain repeated write into one stable logical operation.

How to apply this

  • Define what counts as the same user intent.
  • Store the idempotency record atomically with the side effect when possible.
  • Return the previous result and reject conflicting reuse of a key.
Scenario — Duplicate orders after a timeout

Situation

The first create-order call commits, its response is lost, and the browser sends the request again.

Decision

Require an idempotency key and enforce a unique order operation so both calls resolve to one order.

Chapter 5

Circuit Breakers: Stop Calling a Failing Dependency

A circuit breaker temporarily blocks calls when a dependency repeatedly fails.

A circuit breaker protects capacity. In the closed state, calls pass normally. After failures cross a threshold, it opens and fails fast without contacting the dependency. After a cool-down, a small number of trial calls enter a half-open state to test recovery.

The breaker must be scoped and measured carefully. One global breaker can hide a healthy region because another region is failing. A threshold that is too sensitive causes needless outages, while one that is too slow allows saturation. Breaker events need dashboards and alerts because a fast fallback can otherwise hide a long incident.

If a shipping quote provider is unavailable, an open breaker can stop hundreds of slow calls and show that exact delivery prices are temporarily unavailable. The team can still preserve browsing and carts while the provider recovers.

Key insight

A circuit breaker limits repeated damage; it does not repair the dependency or choose the fallback for you.

How to apply this

  • Scope breakers by dependency and meaningful failure domain.
  • Define closed, open, and half-open behavior explicitly.
  • Monitor open duration, rejected calls, and trial-call outcomes.
Scenario — Slow tax provider consumes all workers

Situation

Each checkout waits on the same failing provider until application workers are exhausted.

Decision

Open a circuit after sustained failures, fail fast, and route users to the agreed fallback or retry-later path.

Chapter 6

Graceful Degradation: Preserve the Core Journey

Graceful degradation keeps the most important user outcome available when a secondary capability fails.

A degraded mode is an intentional lower level of service. Examples include hiding recommendations, serving slightly stale public data, switching to read-only mode, or accepting a request for later processing. It is different from silently returning wrong data; the remaining behavior must still be safe and honest.

Choose degradation by ranking capabilities as critical, important, or optional. Define the trigger, the user message, the data freshness allowed, and the recovery condition. Test the mode before an incident because code paths that are never exercised often fail when needed.

An event site can keep schedules readable from a recent cache while registration storage is unavailable. It must disable the registration button and explain the temporary limitation rather than pretending a booking succeeded.

Key insight

Degrade features, not correctness or user trust.

How to apply this

  • Identify the smallest useful journey that must remain available.
  • State freshness and correctness limits for every fallback.
  • Exercise degraded modes in tests and operational drills.
Scenario — Search outage takes down the store

Situation

The page treats search suggestions as required, so a suggestion outage blocks product browsing.

Decision

Remove suggestions from the critical path and allow direct browsing while showing a small, honest limitation message.

Chapter 7

Queues and Workers: Move Work Out of the Request

A queue stores work for a worker to process later, separating acceptance from completion.

The producer writes a message describing a job. A queue holds it until a consumer or worker claims it. This design shortens user requests and absorbs traffic spikes, but it introduces states such as queued, processing, succeeded, and failed. A response of 202 Accepted means work was accepted, not completed.

Most queues provide at-least-once delivery, so a worker may receive the same message more than once. Workers therefore need idempotency, bounded retries, a dead-letter path, and visibility into queue depth and oldest-message age. Message payloads should carry stable identifiers, not large mutable objects.

For a ninety-second report, the API creates one job and returns its ID. A worker builds the file, stores the result, and updates job status. The UI polls or receives a notification and shows failure with a retry option instead of keeping one HTTP request open.

Key insight

A queue moves time and failure boundaries; it does not remove them.

How to apply this

  • Model visible job states before writing producer code.
  • Make workers safe for duplicate delivery.
  • Monitor queue depth, message age, retries, and dead-letter volume.
Scenario — Email jobs disappear from view

Situation

The API publishes messages but nobody can tell whether workers are delayed or repeatedly failing.

Decision

Add durable job identifiers, explicit states, retry limits, a dead-letter queue, and operational metrics.

Chapter 8

Combine the Tools into a Reliable Async Workflow

After learning each reliability tool separately, combine only the tools required by one user journey.

Begin with the failure model and a latency budget. Use a timeout where waiting must end. Add a retry only for transient errors, and require idempotency before retrying uncertain writes. Use a circuit breaker when repeated calls can exhaust capacity, and define graceful degradation for optional work.

If work is slow or bursty, place it behind a queue and expose honest job states. The worker uses the same idempotency rule, bounded retries, and dead-letter handling. Metrics connect the design: request latency, breaker state, queue age, job failure rate, and the user outcome all need visible thresholds.

Consider image processing after an upload. The API validates and stores the original once, creates one idempotent job, and returns a job ID. Workers generate variants, retry temporary storage errors with backoff, and mark permanent failures for support. The UI can still show the original while processing continues.

Key insight

Combine reliability mechanisms from a named risk and user promise, not from a checklist of fashionable patterns.

How to apply this

  • Write the end-to-end state machine and failure paths.
  • Assign one retry owner and one idempotency boundary for each side effect.
  • Test success, timeout, duplicate delivery, dependency failure, and recovery.
Scenario — Async invoice generation

Situation

Users create duplicate invoices because HTTP retries and worker retries both repeat the side effect.

Decision

Use one intent key from API to worker, persist state transitions, retry only transient work, and expose one invoice status to the user.

Applied exercise

Design a reliable report-export workflow

Replace a slow synchronous export with an observable job that remains correct under retries, duplicate messages, worker crashes, and storage outages.

  1. 01Draw the current request path and list failure modes at every boundary.
  2. 02Define request and worker timeouts, retry rules, and one idempotency key.
  3. 03Design job states, queue behavior, dead-letter handling, and user-visible messages.
  4. 04Specify degraded behavior plus metrics and alerts for queue age and failure rate.
  5. 05Walk through duplicate delivery, a worker crash after upload, and recovery.

Deliverable

A state diagram, failure table, API contract, retry and idempotency policy, and monitoring plan that another engineer could implement.

Knowledge check

Check your understanding before finishing

Answer from the reasoning and trade-offs, not keyword memory. After submitting, review the explanation for every question.

Q1What does a timeout prove?
Q2When is an automatic retry most appropriate?
Q3What is the main purpose of an idempotency key?
Q4Why can a queue deliver the same message more than once?
Q5What should come before combining retries, breakers, and queues?