Production resilience is not about retrying every failure. It is about bounding work, protecting dependencies, and making failure modes predictable. This note gives an interview-ready mental model and a practical .NET implementation checklist.
Start with a timeout budget
Every outbound call should have a deadline shorter than the caller's overall request budget. If an HTTP request has a 2-second service-level objective, do not give a dependency the full 2 seconds: reserve time for serialization, other dependencies, and returning a useful error.
A timeout is a resource boundary, not a recovery policy. It stops waiting; it does not make the operation succeed. Always propagate the request cancellation token so abandoned requests stop consuming sockets, database connections, and CPU.
Retry only transient, safe operations
Good retry candidates include temporary connection failures, HTTP 408, HTTP 429 when the server supplies retry guidance, and selected 5xx responses. Do not automatically retry validation failures, authentication failures, or deterministic business conflicts.
Before retrying a write, verify idempotency. A payment, email, or inventory decrement may be repeated if the first response is lost. Use an idempotency key or an operation identifier stored with the result.
Use exponential backoff with jitter so many instances do not retry together. Keep the attempt count small and ensure the complete retry sequence fits inside the caller's timeout budget.
Circuit breakers protect the dependency
A circuit breaker stops sending calls when failures show that a dependency is unhealthy. In the open state, calls fail quickly. After a cooling period, a limited probe is allowed in the half-open state. A successful probe can close the circuit; another failure opens it again.
The breaker is not a replacement for timeouts. Without a timeout, slow calls can exhaust the connection pool before the breaker observes enough failures.
.NET 10 resilience pipeline
Use the Microsoft.Extensions.Http.Resilience package for standard HTTP resilience policies. Keep the policy close to the typed or named HttpClient so its ownership is obvious.
builder.Services
.AddHttpClient<InventoryClient>(client =>
{
client.BaseAddress = new Uri(builder.Configuration["Inventory:BaseUrl"]!);
})
.AddStandardResilienceHandler(options =>
{
options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(1);
options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(3);
options.Retry.MaxRetryAttempts = 2;
options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(30);
options.CircuitBreaker.FailureRatio = 0.5;
options.CircuitBreaker.MinimumThroughput = 20;
});
Pass cancellation through every layer:
public async Task<InventoryItem?> GetAsync(
string sku,
CancellationToken cancellationToken)
{
using var response = await httpClient.GetAsync(
$"inventory/{Uri.EscapeDataString(sku)}",
cancellationToken);
if (response.StatusCode == HttpStatusCode.NotFound)
return null;
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<InventoryItem>(
cancellationToken: cancellationToken);
}
Common interview traps
- Stacking retries: Application, proxy, and SDK retries multiply. Two retries at three layers can create far more attempts than expected. Define one owner for retry behavior.
- Retrying non-idempotent writes: A timeout means the response is unknown, not that the server did nothing.
- Ignoring Retry-After: Respect server-directed backoff for throttling responses.
- Using one global circuit: Isolate breakers by dependency and, where appropriate, operation. An unhealthy optional endpoint should not block an unrelated critical call.
- Hiding failures: Emit structured logs and metrics for attempts, timeouts, breaker state changes, final outcome, and dependency latency. Avoid logging secrets or complete payloads.
Production checklist
- Define the end-to-end latency budget before individual timeouts.
- Propagate CancellationToken from the HTTP request.
- Reuse HttpClient through IHttpClientFactory.
- Retry only classified transient failures.
- Require idempotency for retried writes.
- Add jitter and cap the total retry duration.
- Use a circuit breaker per dependency boundary.
- Test timeout, throttling, slow-success, and recovery paths.
- Monitor final failures separately from recovered retries.
- Return a controlled fallback only when stale or partial data is safe.
Interview answer in one minute
I start with an end-to-end deadline and propagate cancellation. Each dependency gets a smaller timeout. I retry only transient failures, with jitter and a small attempt count, and only when the operation is idempotent. A circuit breaker fails fast during sustained dependency failure. The policies are observable, tested against injected faults, and configured so retries cannot exceed the request budget.