Modern ASP.NET Core applications rarely operate in isolation. They communicate with databases, payment gateways, cloud storage, authentication providers, messaging systems, and third-party APIs. While these dependencies are essential, they also introduce failures that are outside your application's control.

A temporary network outage, a slow external API, or a throttled cloud service shouldn't immediately cause your application to fail. Instead, applications should recover gracefully, minimize user impact, and protect downstream services from cascading failures.

Polly is the de facto resilience library for .NET, allowing developers to implement retry, circuit breaker, timeout, fallback, and other resilience patterns with minimal code changes. Combined with HttpClientFactory, Polly helps build reliable, production-ready applications.

In this article, you'll learn how to implement resilience policies in ASP.NET Core, understand when to use each strategy, and build a resilient communication pipeline for external services.

Why Resilience Matters

The Reality of Distributed Systems

Consider an order processing application.

Customer
    │
    ▼
ASP.NET Core API
    │
 ┌──┴───────────────┐
 ▼                  ▼
SQL Database   Payment Gateway
                    │
                    ▼
             Shipping Provider

If the payment gateway becomes temporarily unavailable:

Not every failure requires immediate failure. Many are temporary and recover automatically within seconds.

Understanding Retry

What Is Retry?

Retry automatically repeats an operation after a transient failure.

Typical transient failures include:

Instead of failing immediately, the application retries the request after a short delay.

Configuring Retry

Register an HttpClient with a retry policy.

builder.Services
    .AddHttpClient<PaymentClient>()
    .AddPolicyHandler(
        Policy<HttpResponseMessage>
            .Handle<HttpRequestException>()
            .OrResult(r => !r.IsSuccessStatusCode)
            .WaitAndRetryAsync(3,
                retry =>
                    TimeSpan.FromSeconds(retry)));

Why Use Retry?

Many failures are temporary.

Retrying a request a small number of times often succeeds without requiring user intervention.

However, retries should remain limited because excessive retries can increase load on already struggling services.

Understanding Circuit Breaker

Retries alone cannot solve every problem.

If a service remains unavailable, repeatedly retrying requests wastes resources.

Circuit Breaker addresses this issue.

builder.Services
    .AddHttpClient<InventoryClient>()
    .AddPolicyHandler(
        Policy<HttpResponseMessage>
            .Handle<HttpRequestException>()
            .CircuitBreakerAsync(
                5,
                TimeSpan.FromSeconds(30)));

Why Use a Circuit Breaker?

After several consecutive failures:

After the configured break period expires, the circuit allows limited traffic to determine whether the service has recovered.

Configuring Timeouts

Waiting indefinitely for an external service reduces application throughput.

builder.Services
    .AddHttpClient<ShippingClient>()
    .AddPolicyHandler(
        Policy.TimeoutAsync<HttpResponseMessage>(
            TimeSpan.FromSeconds(10)));

Why Use Timeouts?

Timeouts prevent slow external services from consuming request threads indefinitely.

Combined with retries and circuit breakers, they help maintain predictable response times during dependency failures.

Implementing Fallback

Sometimes an alternative response is preferable to a failure.

Policy<string>
    .Handle<Exception>()
    .FallbackAsync("Service temporarily unavailable.");

Why Use Fallback?

Fallback provides a controlled response when every other resilience strategy fails.

Examples include:

Fallback should provide useful behavior rather than simply masking failures.

Combining Policies

Production applications rarely use a single resilience strategy.

Typical execution order:

Request
   │
   ▼
Timeout
   │
Retry
   │
Circuit Breaker
   │
Fallback
   │
External Service

Each policy addresses a different failure scenario.

Together they create a more reliable communication pipeline.

End-to-End Implementation

Consider an online retail platform.

Architecture:

Customer
     │
     ▼
Order API
     │
HttpClientFactory
     │
Polly Policies
     │
 ┌───┴─────────────────────┐
 ▼                         ▼
Payment API         Shipping API

Workflow:

  1. A customer places an order.

  2. The Order API calls the payment service.

  3. If a transient failure occurs, Polly retries the request.

  4. If repeated failures continue, the circuit breaker opens.

  5. During the open state, requests fail immediately without contacting the external service.

  6. If all recovery attempts fail, the fallback policy returns an alternative response.

  7. Once the break interval expires, the circuit allows a limited number of requests to determine whether the external service has recovered.

This layered approach minimizes customer impact while preventing repeated failures from overwhelming dependent systems.

Comparing Resilience Strategies

StrategyBest ForPrevents
RetryTemporary failuresImmediate request failures
Circuit BreakerRepeated failuresCascading failures
TimeoutSlow dependenciesResource exhaustion
FallbackUnrecoverable failuresPoor user experience

Each strategy addresses a different reliability concern, and they are most effective when used together.

Best Practices

Common Mistakes

One common mistake is retrying every exception indiscriminately. Permanent failures, such as authentication errors or invalid requests, should not be retried because they will continue to fail.

Another issue is configuring aggressive retry policies. Multiple retries across several services can amplify traffic and worsen outages rather than improving reliability.

Developers also sometimes treat fallback responses as a replacement for proper error handling. Fallback should provide an acceptable degraded experience, not conceal operational issues.

Testing and Validation

Before deploying resilience policies, verify:

Chaos testing and simulated dependency failures help validate resilience strategies under realistic production conditions.

Performance Considerations

Resilience policies improve reliability, but they should be configured carefully.

Consider these recommendations:

Properly configured resilience policies improve overall system stability without introducing unnecessary processing overhead.

Security Considerations

Resilience mechanisms should not compromise application security.

Follow these recommendations:

Reliability and security should complement each other throughout the application's communication pipeline.

Troubleshooting

Retry Never Executes

Verify that the configured policy handles the specific exception or HTTP status code being returned by the external service.

Circuit Breaker Opens Too Frequently

Review failure thresholds and timeout settings. The configured limits may be too aggressive for normal traffic patterns.

Requests Continue Timing Out

Investigate the downstream service rather than continually increasing timeout values. Persistent timeouts often indicate an underlying performance issue.

Fallback Response Is Never Returned

Ensure the fallback policy wraps the retry, timeout, and circuit breaker policies in the intended execution order.

Conclusion

Building resilient ASP.NET Core applications requires more than handling exceptions. Retry policies recover from transient failures, circuit breakers prevent cascading outages, timeouts protect server resources, and fallback strategies provide graceful degradation when dependencies remain unavailable. By combining Polly with HttpClientFactory and carefully configuring resilience policies, developers can build production-ready applications that remain responsive, reliable, and easier to operate even when external services experience failures.