Skip to content

One service went down. Why did the other eleven follow?

The Failure Should Have Stopped There · Part 3 of 6 · 6 min read

Read the worked explanation ↓

An e-commerce request synchronously calls nine services, while profile checks fail 15% of the time. Three layers each have up to two retries, and surviving backends run at 80% baseline utilization. Trace a plausible cascade and propose a tested rollback and resilient design.

Think first

An e-commerce request synchronously calls nine services, while profile checks fail 15% of the time. Three layers each have up to two retries, and surviving backends run at 80% baseline utilization. Trace a plausible cascade and propose a tested rollback and resilient design.

Take a few minutes to form an approach, then compare it with the worked explanation.

Reveal the worked explanation

Interview question. A subscription platform has twelve synchronous microservices on the request path. A minor profile dependency starts returning errors for only 10% of calls. Within minutes, checkout and login fail across the region. Trace the cascade, identify its feedback loops, and propose an architecture that keeps a small failure small.

At 09:40 the profile service loses one replica. Capacity falls slightly. The system could tolerate that. At 09:41 its p99 rises, a client retries, the next service retries that retry, and the gateway retries the entire request. By 09:45 the authentication team sees its CPU at 100%. Auth was not broken. It was trying to survive a request rate that now included many duplicate attempts.

Map the synchronous request graph, not just the organization chart. A single customer action might call checkout, pricing, identity, profile, promotions and inventory, with some calls in parallel and others serial. For each edge, write down the deadline, retry ownership, retry count, retryable errors, concurrency limit, and whether the data is genuinely required before a customer response. A harmless optional dependency becomes dangerous when the whole graph treats its response as mandatory, holds work while waiting, and then reacts to timeout by adding yet more work.

A failing profile branch amplifies retries and steals shared capacity, while isolation stops the feedback before it spreads to checkout.
A failing profile branch amplifies retries and steals shared capacity, while isolation stops the feedback before it spreads to checkout.

Try a simple calculation. If the gateway, checkout service and profile SDK each allow three total attempts for the same operation, a pathological request could induce up to 3 × 3 × 3 = 27 downstream attempts, subject to which layers actually retry. The exact number is not automatically 27 in every call graph because some attempts fail early and retry policies differ. But multiplicative attempt trees are real when layers are independent. A 10% failure at one endpoint can become much more expensive than a 10% loss of customer requests. Worse, the failures may occur under overload, when additional work is least likely to help.

The second feedback loop is deadline amplification. Profile slows from 50 to 900 ms. Checkout waits, holds a worker and a database connection, then times out after 700 ms. Profile's operation might continue consuming backend CPU after the caller has abandoned it if cancellation is ineffective. When checkout retries, the abandoned operation and new attempt overlap. Resource use goes up while successful useful responses go down.

Now consider load balancing. As profile replicas fail health checks, the gateway pushes traffic to surviving replicas. Their load rises, their latency rises, and they fail too. A naive regional fallback may route to a second cluster that never had spare headroom for the extra work. The apparent high-availability design carries the overload to every healthy region. Google SRE describes these positive-feedback cascades and the importance of rejecting excess work before it consumes scarce resources.

Containment starts at each boundary. Choose one layer to own retries for a user operation, and set a small global attempt or retry budget rather than independent retries at every hop. Use randomized backoff for suitable retriable failures. Apply end-to-end deadlines so a downstream service cannot spend another second on a response the caller no longer needs. Most importantly, gate expensive work with bounded concurrency and queue admission. Overloaded services should be able to reject cheaply and preserve a core rate of useful completions. If a dependency is optional, a carefully designed degraded result avoids the call altogether. If it is mandatory, fail honestly instead of manufacturing data.

A team proposes circuit breakers at every edge. They help suppress calls to known unhealthy dependencies, but they do not solve the graph. Suppose every request requires twelve services to be available, each with independent 99.9% availability. Under an oversimplified independent-failure model, the path's availability is 0.999^12, approximately 98.81%. Real correlated failures make multiplication an inadequate full availability forecast, but the toy model teaches something: a long synchronous critical path creates many opportunities for unavailability. The stronger design removes unnecessary synchronous dependencies and makes noncritical side effects asynchronous with explicit completion and failure semantics.

Dependency importance needs to be an explicit contract. Can the user view an order without current recommendations? Usually yes. Can the user confirm an inventory reservation without any authority on stock? Usually no. Can the user start an asynchronous export while export status has a transient delay? Perhaps, if acceptance is durably recorded and the UI shows pending state. These choices affect system behavior more than installing the same breaker library everywhere. The interview is testing whether you can turn a dependency graph into a business correctness graph.

During the incident, do not randomly turn up all timeouts. Longer timeouts often increase in-flight occupancy and worsen the cascade. Instead, compare original customer request rate, total downstream attempt rate, completed useful operations, queue age, rejected work and each dependency's saturation. Disable unnecessary retries, shed optional expensive work, and route to proven spare capacity only when measured. Recovery is incomplete until the ratio of total attempts to successful customer operations normalizes and the remaining queues drain without creating a fresh wave.

Then rehearse the event. Terminate one profile replica, inject a 900 ms slow-success mode, disable one network path and replay realistic offered traffic. Watch whether the failure remains within profile and optional enrichment, or whether shared thread pools, database connections and regional routing still spread it. Record the first resource that saturates and the first feedback loop that becomes self-reinforcing. Your prevention plan should target those facts, not a generic checklist of microservice resilience patterns.

I would finish the interview by drawing two graphs: the call graph showing where work travels and the dependency graph showing which resources and guarantees are shared. A cascading failure follows both. Breaking the causal path might mean a shorter critical route, bounded retries, admission control, capacity reservations or explicit fallback. The principal-level skill is being able to prove where the failure stops.

Go deeper

Consider a second-order cascade when failover or autoscaling itself imposes new backend traffic. Newly launched instances may miss caches, initialize TLS connections, load model weights, or rebuild pools simultaneously. Effective capacity after a failover can therefore be below the expected steady-state capacity even if the raw instance count is unchanged. Load test failovers with cold-start penalties and measure goodput rather than only attempted RPS. In circuits with cancellation, distinguish caller-side timeout from remote work completion so cost attribution remains accurate.

Check your reasoning

  • Draw synchronous critical-path and shared resource dependencies.
  • Identify nested retry amplification and abandoned work after deadlines.
  • Show health-check and load-balancing redistribution feedback.
  • Assign one retry policy, enforce end-to-end deadlines and bounded admission, and remove optional synchronous calls.
  • Validate prevention and recovery with slow responses, partial replica loss, attempt rates and goodput.

Common mistake: Because profile errors affect only 10% of calls, checkout availability can fall by no more than 10%.

Which changes contain a partial dependency failure before retry and shared-resource feedback spread it?

Read the answer explanation

Bound retries and in-flight work, and stop treating noncritical profile enrichment as a checkout prerequisite — Nested attempts and shared resources can amplify a small downstream failure into a service-wide overload, so containment must target the feedback graph.

Apply it elsewhere

An AI coding agent synchronously calls model inference, context retrieval, policy validation, and a remote sandbox. Model inference becomes slow but keeps succeeding. Identify how the failure can propagate and which operations can be deferred.

Compare your approach

Map critical and optional edges, identify retry and deadline multiplication, explain health-check and failover amplification, size admission and load shedding on useful throughput, and verify the recovery with fault injection and dependency-level metrics.

One more constraint

If three independently nested layers each make up to three total attempts, can one customer operation create up to 27 downstream attempts in a pathological call path?

Quick revision

A cascade grows through feedback: retries add load, slow calls hold scarce resources, unhealthy replicas shift traffic onto fewer survivors, and deadlines cause abandoned work. Draw the call and shared-resource graphs. Assign retry ownership, propagate one end-to-end deadline, cap concurrency and queues, shed cheaply before critical resources collapse, and avoid moving overload into another region. Reduce the synchronous critical path. Validate containment with slow-success, partial-failure and failover experiments while measuring completed useful work.

References