Cloud Architecture Resilience With .NET and Polly

Cloud Architecture Resilience With .NET and Polly

In a cloud environment, failures are not a matter of if, but when. Network latency, service unavailability, and external API timeouts are everyday realities. That’s why building resilient applications is no longer optional, it’s a core requirement for any robust system.

Resilience means your system can bounce back from issues without crashing or affecting the user experience. It’s not about avoiding errors altogether, it’s about handling them smartly so your app stays reliable even when things don’t go as planned.

That’s exactly where Polly comes in. Polly is a .NET library that helps you deal with transient failures using strategies like retries, circuit breakers, timeouts, and fallbacks. It gives you full control over how your app should respond when something goes wrong.

In this post, I’ll walk you through how to use Polly to build more resilient cloud architectures in .NET, with real code examples and lessons from the field.

 

Resilience Strategies You Can Use with Polly

Here are several built-in strategies to help you deal with transient failures and unexpected issues in distributed systems:

  • Retry: The classic “try again” approach. This strategy is great for dealing with temporary issues, like brief network problems. You can set the number of retries and even introduce some randomness (jitter) between attempts to prevent overloading the system if multiple requests happen at once.
  • Circuit Breaker: Think of this like a fuse in your electrical system. If errors accumulate, the circuit breaker “trips,” temporarily stopping calls to the failing service. This gives the system time to recover without overwhelming it.
  • Fallback: Provides a backup response when the primary service call fails. This could be a cached response or a simple message like “service unavailable.” It ensures that your system remains functional even if a service goes down.
  • Hedging: Sends multiple requests at the same time and takes the first successful response. This is useful when your system has multiple ways to handle a request and you want to increase the chances of getting a response quickly.
  • Timeout: Prevents operations from hanging indefinitely by setting a time limit. If the operation exceeds the defined timeout, it’s automatically cancelled, ensuring your app doesn’t get stuck waiting for a response from a slow service.

 

Each of these strategies can be used on its own or combined depending on your needs.

Using Resilience 

In .NET, you can easily integrate Polly’s resilience strategies with the Microsoft.Extensions libraries, which provide a simple way to manage resilience within your application. The two key packages you’ll want to install are:

  1. Microsoft.Extensions.Resilience: This package adds resilience features for any service that needs fault tolerance.

  2. Microsoft.Extensions.Http.Resilience: This is a more specific extension for HTTP client resilience, allowing you to apply Polly strategies like retries and circuit breakers to HTTP requests.

First, you’ll need to install the required NuGet packages. You can do this via the NuGet Package

PowerShell
				dotnet add package Microsoft.Extensions.Resilience
dotnet add package Microsoft.Extensions.Http.Resilience
			

Once the packages are installed, we can start by creating our first ResiliencePipeline. We’ll implement a simple pipeline that combines a Retry strategy with a Timeout strategy and Circuit Breaker strategy, ensuring that if a request fails, it retries a few times before timing out and Circuit Breaker prevents hammering a failing service and lets it recover gracefully.

C#
				var pipeline = new ResiliencePipelineBuilder<IEnumerable<Notice>>()
                    .AddRetry(new RetryStrategyOptions
                    {
                        ShouldHandle = new PredicateBuilder().Handle<HttpRequestException>(),
                        Delay = TimeSpan.FromSeconds(1),
                        MaxRetryAttempts = 3,
                        BackoffType = DelayBackoffType.Exponential,
                        UseJitter = true
                    })
                    .AddTimeout(new TimeoutStrategyOptions
                    {
                        Timeout = TimeSpan.FromSeconds(10)
                    })
                    .AddCircuitBreaker(new CircuitBreakerStrategyOptions
                    {
                        BreakDuration = TimeSpan.FromSeconds(30),
                        FailureRatio = 0.5,
                        MinimumThroughput = 10,
                        OnOpened = args =>
                        {
                            // Log or notify when the circuit opens
                            return ValueTask.CompletedTask;
                        },
                        OnClosed = args =>
                        {
                            // Log or notify when the circuit closes
                            return ValueTask.CompletedTask;
                        },
                        OnHalfOpened = args =>
                        {
                            // Log or notify when the circuit is half-open
                            return ValueTask.CompletedTask;
                        }
                    })
                    .Build();

var response = await pipeline.ExecuteAsync(
    async token => await noticeServiceRest.GetNoticeAsync(request),
    CancellationToken.None // (default) or set your own token
);

			

These strategies together creates a resilient and responsive pipeline:

  • Retry handles transient failures.

  • Timeout avoids hanging operations.

  • Circuit Breaker prevents cascading failures when something is truly broken.

Together, they create a safety net for unpredictable network conditions and service instability, a must-have for any cloud-based system.

The Circuit Breaker has three states:

  1. Closed: Requests are allowed to pass. If failures exceed a threshold, it moves to the Open state.

  2. Open: Requests are blocked to prevent overwhelming the failing service. After a set time, it transitions to Half-Open.

  3. Half-Open: A few requests are allowed to test if the service has recovered. If successful, the circuit closes; if failures continue, it opens again.

Here’s a diagram of how it works to make it easier for you to understand:

Circuit Breaker - Polly

Adding Resilience to HttpClient with AddStandardResilienceHandler

If you’re using HttpClient in your .NET apps, there’s an easier and more elegant way to apply resilience: using the built-in AddStandardResilienceHandler method. This is a good approach, it’s clean, reusable, and integrates directly into the DI container.

Instead of manually building a pipeline, you configure it once when registering your HttpClient:

C#
				services.AddHttpClient<INoticeServiceRest, NoticeServiceRest>(client =>
{
    client.DefaultRequestHeaders.Clear();
    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
})
.AddStandardResilienceHandler();
			

The standard resilience pipeline includes these strategies:

  • Limiter: Restricts the number of concurrent requests to the dependency.

  • Request Timeout: Applies a timeout for the entire request, including retries.

  • Retry: Automatically retries a request if it fails due to a transient error or timeout.

  • Circuit breaker: Stops further requests when a predefined number of failures occur.

  • Attempt timeout: Sets a timeout for individual requests to prevent long waits.

Adding Custom Pipeline With DI

First, define your custom resilience pipeline and add it to the DI container:

C#
				servicos.AddResiliencePipeline<string, IEnumerable<Notice>>("notice-retry", builder =>
{
	builder.AddRetry(new Polly.Retry.RetryStrategyOptions<IEnumerable<Notice>>
	{
		Delay = TimeSpan.FromSeconds(1),
		MaxRetryAttempts = 2,
		BackoffType = DelayBackoffType.Exponential,
		UseJitter = true
	});
});
			

With the custom pipeline configured in the DI container, you can inject your service and execute the resilience logic, this approach streamlines the setup, making your application more resilient while keeping the code clean and maintainable.

C#
				//In Primary Constructor add the pipeline provider
public class MyClass(ResiliencePipelineProvider<string> pipelineProvider /*, ...*/ ) :

//Method
{
    //...

    ResiliencePipeline<IEnumerable<Notice>> pipeline =
            pipelineProvider.GetPipeline<IEnumerable<Notice>>("notice-retry");
    
    var response = await pipeline.ExecuteAsync(
        async token => await noticeServiceRest.GetNoticeAsync(request),
        CancellationToken
    );

    //...
}
			

Conclusion

Building resilient cloud applications is essential in today’s interconnected world where services are often distributed and external dependencies may fail unexpectedly. By leveraging strategies like Retry, Timeout, and Circuit Breaker, you can ensure your application can handle transient failures and maintain a smooth user experience.

Integrating resilience directly into your services using libraries like Polly and Microsoft.Extensions.Resilience is an efficient way to manage fault tolerance across your entire application. Whether you implement a custom resilience pipeline or use built-in solutions via Dependency Injection (DI), you can easily enforce reliable and robust behavior in your APIs and microservices.

Remember, adding resilience doesn’t just prevent failures, it helps you build more robust and user-friendly applications that can better withstand the unpredictable nature of cloud environments. Don’t wait for failures to impact your users, make resilience a core part of your application design.

What resilience strategies have you found most effective in your applications?

Thank you for reading.

See you next time!

Share the Post:
plugins premium WordPress