Resilience questions separate engineers who've configured a retry policy from engineers who've watched a poorly configured one turn a five-minute blip into a two-hour outage. At the senior level, interviewers assume you know retries and circuit breakers exist; what they're actually probing is whether you understand the failure mechanics well enough to combine these patterns correctly — because combined badly, retries, timeouts and circuit breakers can make an incident worse, not better. This page works through backoff and jitter, circuit breakers and bulkheads, timeout budgets across a call chain, hedging and fallbacks, retry storms, and how Polly v8 and Microsoft.Extensions.Http.Resilience implement all of it in current .NET code.

Q1 Why does a naive retry-on-failure loop make outages worse, and how do backoff and jitter fix that?#

Short answer: A fixed, immediate retry synchronizes every failing client into hammering the same struggling dependency at the same instant, turning a partial slowdown into a full outage — the dependency never gets room to recover because the retry traffic itself becomes the dominant load. Exponential backoff spaces each client's retries further apart over successive attempts so pressure decreases over time; jitter randomizes the exact delay so clients that failed at the same moment don't all retry at the same moment either.

Picture 1,000 clients calling a database that starts timing out in the same second. If every client retries after a fixed one-second delay, you get 1,000 more calls exactly one second later, then 1,000 more a second after that — an unbroken wave that never lets connection pools or query queues drain. Exponential backoff, doubling the delay on each attempt, thins that wave over time. Jitter breaks the "everyone retries at the exact same instant" synchronization by adding randomness to each client's individual delay, so those 1,000 retries spread across a window instead of landing as one spike layered on top of the last.

This isn't something you need to hand-roll with Thread.Sleep and a random number generator — Polly v8's retry strategy supports exponential backoff with jitter as direct configuration.

C#
var pipeline = new ResiliencePipelineBuilder<HttpResponseMessage>()
    .AddRetry(new RetryStrategyOptions<HttpResponseMessage>
    {
        MaxRetryAttempts = 4,
        BackoffType = DelayBackoffType.Exponential,
        UseJitter = true,
        Delay = TimeSpan.FromMilliseconds(200)
    })
    .Build();

What interviewers look for: the thundering-herd mechanism specifically, not just "retries can overload things," and understanding that jitter solves a different half of the problem than backoff — desynchronizing clients rather than spacing attempts over time.

Common mistakes: implementing backoff without jitter and still seeing correlated retry spikes because every client computes the same delay sequence from the same failure moment.

Q2 Explain how a circuit breaker works and how you'd choose its thresholds.#

Short answer: A circuit breaker tracks recent failures for calls passing through it, and once the failure rate crosses a configured threshold within a sampling window, it opens and fails every subsequent call immediately, without attempting the real call, for a configured break duration — giving the struggling dependency room to recover instead of continuing to absorb load from every caller. After the break duration it moves to half-open, lets a small number of trial calls through, and closes again if they succeed or re-opens if they don't.

The three states matter because fail-fast isn't just declining to help the caller — it actively protects the downstream dependency from load it currently can't handle, and it protects the caller too, since failing in microseconds beats waiting out a full timeout on every call while the dependency is down. Closed is the normal state: calls pass through and failures are counted. Open means calls fail immediately with no real attempt. Half-open uses a limited number of probe calls to decide whether recovery has actually happened.

Threshold choice is a genuine trade-off. Too sensitive — a low failure-rate threshold or a short sampling window — and ordinary, expected transient failures trip the breaker unnecessarily, adding unavailability that wasn't really there. Too lenient, and the breaker doesn't open until real damage, such as thread-pool exhaustion or cascading timeouts, has already happened upstream. Polly v8's circuit breaker strategy is configured with a failure ratio, a minimum throughput so a handful of calls in a quiet period can't trip it on their own, a sampling duration and a break duration; Microsoft.Extensions.Http.Resilience's standard handler ships with roughly a 10% failure-ratio default as a reasonable starting point, tuned per dependency from there.

What interviewers look for: the three-state model and the fail-fast-protects-both-sides framing, plus recognition that thresholds get tuned per dependency rather than copied from a default.

Follow-up questions:

  • How would you choose different thresholds for a critical payment dependency versus a non-critical recommendations service?

Q3 What is a bulkhead, and what failure does it actually prevent that a circuit breaker doesn't?#

Short answer: A bulkhead limits how much concurrent work — connections, threads, in-flight requests — a single dependency or operation can consume, so that if it becomes slow, the resources it exhausts don't spill over and starve unrelated work in the same process. A circuit breaker decides whether to keep calling a dependency at all, based on its failure rate; a bulkhead limits how much of your own process's capacity that dependency is allowed to consume at once, whether it's currently healthy or not. They solve different problems and are typically used together.

The specific failure a bulkhead prevents is resource exhaustion cascading into unrelated functionality — the classic case is thread-pool starvation, where one slow, non-critical dependency ties up enough threads or connections that a completely unrelated, critical code path can't get a thread either, even though that path never calls the slow dependency at all. A circuit breaker doesn't prevent this by itself, because it only trips once enough failures have accumulated, and by that point the resource exhaustion may already have happened; a bulkhead caps concurrency to that dependency up front, independent of whether it's currently failing.

Polly v8 doesn't have a separate named "Bulkhead" strategy the way Polly v7 did — that capability now lives inside the rate limiter strategy, which can enforce a concurrency limit backed by the .NET rate-limiting primitives, giving the same isolation under a proactive strategy rather than a bespoke bulkhead policy.

What interviewers look for: the specific "resource exhaustion isolation between unrelated work" framing, clearly distinguished from a circuit breaker's failure-rate-based decision.

Common mistakes: treating circuit breakers and bulkheads as interchangeable resilience tools instead of complementary ones addressing different failure mechanics.

Q4 How do you design a timeout budget across a call chain of several services?#

Short answer: Allocate a total time budget at the entry point of a request, and make sure every downstream timeout is strictly smaller than the time remaining when it's invoked, so a slow leaf service fails fast enough for its caller to still do something useful — retry, fall back, return a partial result — inside the overall budget. If every service in the chain independently picks its own generous timeout with no awareness of the total, the end-to-end latency of a single failing request can silently become the sum of every layer's timeout.

Concretely: if the client-facing budget is 2 seconds across Gateway → Service A → Service B, and each layer independently sets a 2-second timeout for its own outbound call, a single slow call to B can take up to 6 seconds before the gateway ever gives up. Each layer's timeout has to leave headroom for the layers above it — a sane allocation might be roughly 500 ms for B, 1,200 ms for A's call to B (leaving room for A's own processing), and the full 2,000 ms as the gateway's outer bound.

This needs to be a deliberate, propagated concept rather than something each service guesses independently — passing a deadline through the call chain, for example via a header the gateway sets that each hop uses when creating its own cancellation token, keeps every layer honest about how much time is genuinely left instead of each one assuming it owns the whole budget.

C#
var remaining = deadline - DateTimeOffset.UtcNow;
using var cts = new CancellationTokenSource(remaining > TimeSpan.Zero ? remaining : TimeSpan.Zero);
var response = await client.GetAsync(requestUri, cts.Token);

What interviewers look for: the "sum of independent timeouts can exceed the intended budget" failure mode specifically, and a mechanism — a propagated deadline — for keeping every hop aware of the real remaining time.

Common mistakes: setting the same generous timeout independently at every layer, which multiplies worst-case latency across the chain instead of bounding it.

Q5 What is hedging, and when is it worth the extra load it creates?#

Short answer: Hedging issues a second parallel attempt at a slow request before the first has failed — after a threshold delay, not after an outright error — and takes whichever response comes back first, trading extra load for lower tail latency. It's worth it when the operation is idempotent, cheap relative to the cost of a slow response, and the dependency has spare capacity to absorb the extra attempts; it's a poor fit for expensive, non-idempotent or already-saturated dependencies, where the duplicate load makes the underlying slowness worse.

The difference from a retry is timing and intent: a retry reacts to a failure that already happened, while a hedge reacts to slowness, firing a parallel attempt while the first is still technically in flight, specifically to cut tail latency rather than recover from an error. Polly v8's hedging strategy configures a delay before issuing the hedged attempt and a maximum number of hedged attempts, and — like retries — it only makes sense for calls safe to execute more than once concurrently, since both attempts may genuinely complete.

The cost side is real. Hedging a request to a dependency already near capacity doesn't just add a bit of load — it can meaningfully worsen the exact slowness you're trying to route around, and duplicate writes need the same idempotency-key treatment duplicate messages need in a messaging system. Hedging tends to be used surgically, on a small number of latency-critical reads against a dependency with clear spare capacity, not applied blanket across every outbound call.

What interviewers look for: distinguishing hedging, which reacts to slowness in parallel, from retry, which reacts to failure sequentially, and naming idempotency and dependency headroom as the preconditions for using it safely.

Follow-up questions:

  • How would you hedge a call safely if the underlying operation isn't naturally idempotent?

Q6 How do you design a good fallback, and when is "fail fast with no fallback" the right answer?#

Short answer: A good fallback returns something genuinely useful and clearly degraded — cached or default data, a simplified response, a state the UI can render meaningfully — rather than papering over a real failure with data the caller can't distinguish from a correct answer. Fail fast with no fallback is correct whenever a wrong or stale answer is worse than an honest error — payment authorization, an inventory count that gates an irreversible action, anything where acting on stale or default data creates a bigger problem than telling the caller the operation didn't complete.

The test to apply at each call site is what the caller actually does with a fallback value, and whether that's better than an explicit failure. For a recommendations widget, an empty or cached list is obviously better than a broken page. For "can this order be fulfilled," a fallback of "assume yes" can make a promise the business can't keep, while a fallback of "assume no" turns a transient blip into lost sales — the honest answer there is often no fallback at all, surfacing the failure and letting the caller or a human decide, rather than silently picking a default that's wrong in one direction or the other.

C#
var pipeline = new ResiliencePipelineBuilder<IReadOnlyList<Recommendation>>()
    .AddTimeout(TimeSpan.FromMilliseconds(300))
    .AddFallback(new FallbackStrategyOptions<IReadOnlyList<Recommendation>>
    {
        FallbackAction = _ => Outcome.FromResultAsValueTask((IReadOnlyList<Recommendation>)[])
    })
    .Build();

Polly v8's fallback strategy accepts either a substitute value or a delegate that produces one, evaluated only when the wrapped strategies ultimately fail, which keeps the fallback logic colocated with the rest of the resilience pipeline instead of scattered through business code as ad hoc try/catch blocks.

What interviewers look for: a concrete per-call-site test for whether a fallback is safe, rather than a blanket "always have a fallback" rule, plus awareness that a wrong fallback can be worse than an honest failure.

Q7 What is a retry storm, and how have you seen — or would you prevent — one in production?#

Short answer: A retry storm is a self-reinforcing overload where retries from a failing dependency's callers become a significant fraction of that dependency's total load, so the retries themselves prevent recovery — the dependency struggles, calls fail, clients retry, load rises, more calls fail, more retries fire, and the system never gets a quiet moment to catch up even after the original trigger is gone. It's prevented with several tools used together: bounded retry counts, exponential backoff with jitter, and a circuit breaker that stops retrying altogether once failures cross a threshold, so retry volume drops as the outage deepens instead of climbing.

The insidious part is that a retry storm can outlive its original trigger — a brief network blip or a deploy-related restart causes an initial wave of failures, and if every client retries aggressively without a circuit breaker, the retry traffic alone stays large enough to keep the dependency saturated well after the original blip has passed, making the incident look far longer than its root cause actually was. Retrying blindly at every layer of a call chain compounds this: if a gateway retries its call to service A, and A independently retries its call to B, a single failure at B can multiply into far more retry traffic at B than either layer's own retry count would suggest, because the multiplication compounds across hops.

The combination that actually prevents this in practice is bounded retries with backoff and jitter, a circuit breaker wrapping the same call so retries stop entirely once the failure rate is clearly systemic, and being deliberate about which single layer in a multi-hop chain is allowed to retry at all — often only the outermost layer should, with inner layers failing fast.

What interviewers look for: the self-reinforcing, outlives-its-trigger dynamic specifically, and the retry-multiplication-across-hops risk in a multi-layer chain — the detail that separates real incident experience from textbook recall.

Common mistakes: retrying independently at every layer of a call chain, which multiplies retry volume at the innermost dependency far beyond what any single layer's own retry count implies.

Q8 Walk through building a resilience pipeline with Polly v8.#

Short answer: Polly v8 centers on ResiliencePipelineBuilder, which composes strategies — retry, timeout, circuit breaker, hedging, fallback, rate limiter — into a single ResiliencePipeline (or a generic ResiliencePipeline<TResult> for typed results) that you build once and execute repeatedly. Strategies are added in the order you want them applied, and each new strategy wraps around the ones already added, so the first strategy added ends up innermost, closest to the actual call.

This pipeline-first design replaced Polly v7's separate policy and policy-wrap types with one composable builder, and it's built for allocation-efficient, high-throughput use: a pipeline is meant to be built once, often registered through dependency injection, and executed many times via ExecuteAsync rather than rebuilt per call.

C#
var pipeline = new ResiliencePipelineBuilder<HttpResponseMessage>()
    .AddRetry(new RetryStrategyOptions<HttpResponseMessage>
    {
        ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
            .HandleResult(r => !r.IsSuccessStatusCode)
            .Handle<HttpRequestException>(),
        MaxRetryAttempts = 3,
        BackoffType = DelayBackoffType.Exponential,
        UseJitter = true
    })
    .AddCircuitBreaker(new CircuitBreakerStrategyOptions<HttpResponseMessage>
    {
        FailureRatio = 0.5,
        MinimumThroughput = 10,
        SamplingDuration = TimeSpan.FromSeconds(30),
        BreakDuration = TimeSpan.FromSeconds(15)
    })
    .AddTimeout(TimeSpan.FromSeconds(5))
    .Build();

var response = await pipeline.ExecuteAsync(
    async ct => await httpClient.GetAsync("/catalog/items", ct), cancellationToken);

Ordering matters and is worth being able to reason about out loud: in this example, retry wraps circuit breaker wraps timeout, so each retry attempt goes through the circuit breaker check and is individually bounded by the five-second timeout. If the circuit breaker were placed outside the retry instead, a single open-circuit failure would immediately stop all retry attempts rather than letting the retry strategy make its own decision per attempt — usually the wrong behavior.

What interviewers look for: the builder-and-composition model, correct ordering intuition (the strategy added first sits closest to the call), and registering pipelines once instead of rebuilding them per request.

Q9 What does Microsoft.Extensions.Http.Resilience's standard handler give you out of the box, and when would you build a custom pipeline instead?#

Short answer: AddStandardResilienceHandler on an HttpClient builder wires up five strategies in one call, outermost to innermost: a rate limiter for concurrency, a total-request timeout covering the whole call including retries, a retry strategy with exponential backoff, a circuit breaker, and a per-attempt timeout — all with sensible, documented defaults such as a 30-second total timeout, three retries, roughly a 10% circuit-breaker failure threshold, and a 10-second per-attempt timeout. You'd reach for AddResilienceHandler and a custom pipeline instead when a specific dependency's failure characteristics genuinely differ from that default shape — a call that legitimately takes longer than the standard total timeout, an endpoint where hedging matters more than retrying, or a client that needs retries disabled for non-idempotent HTTP methods.

C#
services.AddHttpClient<CatalogClient>(c => c.BaseAddress = new Uri("https://catalog"))
    .AddStandardResilienceHandler(options =>
    {
        options.Retry.MaxRetryAttempts = 4;
        options.CircuitBreaker.FailureRatio = 0.15;
    });

services.AddHttpClient<PaymentsClient>(c => c.BaseAddress = new Uri("https://payments"))
    .AddResilienceHandler("payments-pipeline", builder =>
    {
        builder.AddTimeout(TimeSpan.FromSeconds(8));
        builder.AddRetry(new HttpRetryStrategyOptions { MaxRetryAttempts = 1 });
    });

The standard handler exists specifically so most HTTP calls in an application don't need bespoke resilience configuration — a sensible, battle-tested default beats fifty slightly different hand-tuned pipelines scattered across fifty HttpClient registrations. A common reason to drop to a custom pipeline is idempotency: retrying a POST that isn't idempotent risks the same duplicate-side-effect problem covered under messaging patterns, so a payment or order-creation client often needs a deliberately more conservative pipeline than the general-purpose default.

What interviewers look for: knowing the standard handler's five-strategy chain and rough defaults specifically, and a real criterion — differing failure characteristics, non-idempotent methods, hedging needs — for overriding it rather than using it everywhere out of habit.

Q10 How do resilience policies interact badly with each other if ordered wrong?#

Short answer: Ordering determines scope — a strategy added later in the pipeline wraps everything added before it, so it applies to the combined behavior of everything inside it, not to a single raw call. Get this backward and you get failure modes that look like misconfiguration but are actually a correct implementation of the wrong order: a circuit breaker placed outside a retry stops all retries the instant it opens, instead of letting individual attempts fail on their own; a timeout placed outside a retry bounds the entire retry sequence rather than each individual attempt, so a short intended per-attempt timeout quietly becomes "however many attempts fit in this one window."

The generally correct order for an outbound call, outermost to innermost, is roughly: a rate limiter or concurrency cap deciding whether to even attempt the call, a total timeout bounding the whole operation including every retry, retry attempting the call multiple times, a circuit breaker failing fast per attempt once the dependency is clearly down, and a per-attempt timeout bounding one single try — which is exactly the shape AddStandardResilienceHandler builds by default. Recognizing that shape is a strong signal you understand why the order matters, not just that you've memorized it.

The interaction worth explaining unprompted is retry versus circuit breaker: if the circuit breaker sits outside the retry, one open-circuit rejection short-circuits the entire retry budget for that call, which is usually not what you want, since the retry strategy should get to make its own per-attempt decision and only the circuit breaker's fast-fail should stop an individual try. Conversely, if a timeout sits outside retry instead of wrapping each attempt, a caller expecting "this call takes at most five seconds" can end up waiting five seconds multiplied by however many retries fit inside that window — a very different latency contract than the one they thought they configured.

What interviewers look for: the general principle that a strategy added later has outer scope, applied correctly to the retry/circuit-breaker and retry/timeout interactions specifically, since those are the two orderings most often gotten backward in real configurations.

Common mistakes: placing a circuit breaker outside a retry and being surprised retries stop working the moment the circuit opens; placing a timeout outside a retry and being surprised effective per-call latency is a multiple of the configured timeout.

Quick-Fire Round#

QuestionAnswer
What two things does jitter add to backoff?Randomized delay, breaking retry synchronization across clients.
What are a circuit breaker's three states?Closed, open, half-open.
What does a bulkhead limit that a circuit breaker doesn't?Concurrent resource usage (connections/threads) for a specific dependency.
Why can independent per-layer timeouts break a latency budget?They add up across hops instead of leaving headroom for callers above them.
What precondition does hedging require?The operation must be safe to execute more than once (idempotent).
What does Polly v8 use instead of a separate Bulkhead strategy?The rate limiter strategy's concurrency limit.
What five strategies does AddStandardResilienceHandler chain?Rate limiter, total timeout, retry, circuit breaker, attempt timeout.
Where should a circuit breaker sit relative to a retry?Inside it, so it fails fast per attempt without cancelling the whole retry budget.

How to Prepare#

  • Practice explaining thundering herd and retry storms as distinct but related failure modes, with the specific mechanism for each.
  • Know the three circuit breaker states cold, and be ready to justify a threshold choice for a specific dependency.
  • Be able to sketch a timeout budget across a three-hop call chain with real numbers, not just "add timeouts everywhere."
  • Rehearse the retry-versus-circuit-breaker and retry-versus-timeout ordering questions specifically — they come up often as follow-ups.
  • Memorize the AddStandardResilienceHandler five-strategy chain and its rough defaults.
  • Bring one real incident story involving retries, a circuit breaker or a bulkhead — interviewers weight lived debugging over textbook recall.