An ASP.NET Core application often depends on external services to complete a request. It might call a payment gateway, retrieve customer information from another API, or send data to a third-party service. When these dependencies become slow or temporarily unavailable, the application's response time and reliability can suffer.
A common mistake is to send an HTTP request and wait for it to finish without defining how long the application should wait or what should happen when the request fails.
Retries and timeouts help address these problems, but they must be configured carefully. Retrying every failure can overload an already struggling service, while excessively long timeouts can consume application resources. Retrying a non-idempotent operation can also create duplicate transactions.
ASP.NET Core provides IHttpClientFactory for managing HTTP clients, while the standard resilience libraries provide policies for timeouts, retries, and other transient failures.
This article explains how to configure outbound HTTP requests, apply retries and timeouts, handle common failures, and choose settings suitable for production applications.
Why Outbound HTTP Requests Need Resilience
An HTTP request can fail for several reasons:
The remote service is temporarily unavailable.
A network connection is interrupted.
The remote service responds more slowly than expected.
The service returns a temporary error such as HTTP 503.
The application reaches a configured timeout.
The remote service rejects the request because of authentication or validation errors.
Not all failures should trigger a retry. A temporary service outage might justify another attempt, but an HTTP 400 response usually indicates invalid input that will not become valid by repeating the request.
A resilient HTTP client needs to distinguish transient failures from permanent ones and place a limit on the time and number of attempts.
Configure IHttpClientFactory
IHttpClientFactory provides a central place to configure HTTP clients, manage their underlying handlers, and apply logging and resilience policies.
Install the standard HTTP resilience package:
dotnet add package Microsoft.Extensions.Http.ResilienceThis package provides resilience integration for HttpClient, including support for retry and timeout strategies.
Next, register a named client in Program.cs.
using Microsoft.Extensions.Http.Resilience;
using Polly;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddHttpClient("CatalogApi", client =>
{
client.BaseAddress =
new Uri("https://catalog.example.internal/");
client.Timeout = Timeout.InfiniteTimeSpan;
})
.AddStandardResilienceHandler(options =>
{
options.TotalRequestTimeout.Timeout =
TimeSpan.FromSeconds(10);
options.AttemptTimeout.Timeout =
TimeSpan.FromSeconds(3);
options.Retry.MaxRetryAttempts = 2;
options.Retry.Delay =
TimeSpan.FromMilliseconds(300);
options.Retry.BackoffType =
DelayBackoffType.Exponential;
options.Retry.UseJitter = true;
});
var app = builder.Build();
app.MapGet("/", () => "API is running.");
app.Run();The example configures a client for a fictional catalog service. Replace the base address with the actual service endpoint.
The important settings are:
AddHttpClientregisters a client managed by the factory.Timeout.InfiniteTimeSpandisables theHttpClient-level timeout so that the resilience handler controls the request deadline.TotalRequestTimeoutlimits the overall time spent on the outbound operation, including retry attempts and their delays.AttemptTimeoutlimits an individual attempt.MaxRetryAttempts = 2allows up to two retries after the initial attempt.Exponential backoff increases the delay between retries.
Jitter adds variation to retry delays, reducing the chance that many clients retry simultaneously.
The total timeout must be large enough to accommodate the attempt timeouts and retry delays. The outer deadline still takes precedence if the operation takes too long.
The standard resilience handler also includes other strategies, such as a circuit breaker and rate limiting. Review the defaults and adjust them to the dependency's behavior rather than assuming every service needs identical settings.
Understand How Retry and Timeout Policies Work
A retry policy repeats an operation after a qualifying failure. A timeout policy limits how long an operation can continue.
These strategies solve different problems.
Strategy | Purpose | Example |
|---|---|---|
Attempt timeout | Limits the duration of one attempt | Stop waiting after three seconds |
Total request timeout | Limits the entire operation | End the operation after ten seconds |
Retry | Repeats eligible transient failures | Retry a temporary HTTP 503 response |
Circuit breaker | Temporarily blocks calls to an unhealthy dependency | Stop sending requests after repeated failures |
Rate limiter | Limits concurrent or incoming operations | Prevent excessive outbound concurrency |
Retries do not make a remote service available. They provide another opportunity for a temporary failure to recover. If the dependency remains unhealthy, repeated attempts can increase load and make the problem worse.
Use the Configured Client in an ASP.NET Core Service
Once the client is registered, inject IHttpClientFactory into the service that calls the remote API.
using System.Net;
using System.Net.Http.Json;
public sealed class CatalogService
{
private readonly IHttpClientFactory _httpClientFactory;
public CatalogService(
IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task<ProductDto?> GetProductAsync(
int productId,
CancellationToken cancellationToken = default)
{
if (productId <= 0)
{
throw new ArgumentOutOfRangeException(
nameof(productId));
}
var client =
_httpClientFactory.CreateClient("CatalogApi");
using var response = await client.GetAsync(
$"api/products/{productId}",
cancellationToken);
if (response.StatusCode == HttpStatusCode.NotFound)
{
return null;
}
response.EnsureSuccessStatusCode();
return await response.Content
.ReadFromJsonAsync<ProductDto>(
cancellationToken: cancellationToken);
}
}
public sealed record ProductDto(
int Id,
string Name,
decimal Price);
Join the conversation! Your thoughts help the community grow.