Every outbound HTTP call to another service will eventually time out, get rate-limited, or fail transiently. The question isn't whether that happens — it's whether your app has a plan for it. Microsoft.Extensions.Http.Resilience, built on Polly, is the current standard way to add that plan to HttpClientFactory in .NET.

Key takeaways
  • AddStandardResilienceHandler() gives you retry, circuit breaker, timeout, and concurrency limiting on an HttpClient in one call, with sensible defaults.
  • Build a custom pipeline when the standard handler's defaults don't fit — different retry counts for idempotent vs non-idempotent calls, for example.
  • Split health checks into liveness (is the process alive) and readiness (is it able to serve traffic right now) — orchestrators use them differently.
  • Outbound resilience (retries, circuit breakers) and inbound rate limiting are two sides of the same problem — protecting yourself from downstream failures, and protecting your own service from being overwhelmed.

The standard resilience handler

csharp
builder.Services.AddHttpClient("inventory-api", client =>
{
    client.BaseAddress = new Uri(builder.Configuration["InventoryApi:BaseUrl"]!);
})
.AddStandardResilienceHandler();

One line adds a pipeline with retry (with jitter), a circuit breaker, a per-attempt timeout, an overall request timeout, and a concurrency limiter — tuned defaults that cover most outbound calls without hand-configuring each strategy.

Customizing the pipeline

When the defaults don't fit — for example, you only want retries on GET requests, not on a POST that isn't idempotent — configure the pipeline explicitly.

csharp
builder.Services.AddHttpClient("inventory-api")
    .AddResilienceHandler("inventory-pipeline", pipeline =>
    {
        pipeline.AddRetry(new HttpRetryStrategyOptions
        {
            MaxRetryAttempts = 3,
            BackoffType = DelayBackoffType.Exponential,
            ShouldHandle = args => ValueTask.FromResult(
                args.Outcome.Result?.RequestMessage?.Method == HttpMethod.Get &&
                (args.Outcome.Result?.StatusCode is HttpStatusCode.ServiceUnavailable
                    or HttpStatusCode.GatewayTimeout))
        });

        pipeline.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
        {
            FailureRatio = 0.5,
            SamplingDuration = TimeSpan.FromSeconds(30),
            MinimumThroughput = 10,
        });

        pipeline.AddTimeout(TimeSpan.FromSeconds(5));
    });

Liveness vs readiness health checks

Liveness answers "is the process healthy enough to keep running" — a failing liveness check tells the orchestrator to restart the container. Readiness answers "should traffic be routed here right now" — a failing readiness check pulls the instance out of rotation without restarting it. Mixing the two into one /health endpoint causes orchestrators to restart healthy instances just because a downstream dependency is temporarily down.

csharp
builder.Services.AddHealthChecks()
    .AddCheck("self", () => HealthCheckResult.Healthy(), tags: ["live"])
    .AddNpgSql(connectionString, tags: ["ready"])
    .AddUrlGroup(new Uri("https://inventory-api/health"), name: "inventory-api", tags: ["ready"]);

app.MapHealthChecks("/health/live", new HealthCheckOptions { Predicate = c => c.Tags.Contains("live") });
app.MapHealthChecks("/health/ready", new HealthCheckOptions { Predicate = c => c.Tags.Contains("ready") });

Liveness should only check things that indicate the process itself is broken — deadlocked, out of memory. Readiness should check the dependencies that are actually required to serve a request — database connectivity, critical downstream APIs.

Watch forDownstream health checks (AddUrlGroup for another service) make your readiness endpoint only as available as the least reliable thing you depend on. If a non-critical downstream dependency going down shouldn't take you out of rotation, don't tag its check as ready — log it and degrade gracefully instead.

The inbound counterpart: rate limiting

Resilience handlers protect you from downstream failures. ASP.NET Core's built-in rate limiting middleware protects you from being overwhelmed by your own callers — a fixed window, sliding window, token bucket, or concurrency limiter applied to inbound requests, configured once in Program.cs with AddRateLimiter and applied per-endpoint or globally.

Need help with this in production?

astrivo helps teams design, build, and modernize systems like this. Let's talk about your context.