Retry an HTTP call
When you call an HTTP API, transient failures - such as a 503 Service Unavailable response or a dropped connection - can cause your application to fail. To make your application more resilient, you can implement a retry policy that automatically attempts the call again before giving up.
Implementation example
The following example shows how to wrap an HTTP call in a resilience policy using TryRunAsync.
private static async Task<Order?> ReadOrderAsync(HttpClient client, string id, CancellationToken cancellationToken)
{
// Resilience.Http knows that a 503 is transient, a 429 is throttling and a 404 is an
// answer. Three attempts, a 30 s deadline and a 10 s attempt ceiling are the defaults.
var api = Resilience.Http with { Deadline = TimeSpan.FromSeconds(value: 10) };
var result = await api.TryRunAsync(
attempt => client.GetFromJsonAsync<Order>(requestUri: new Uri(uriString: $"https://api.example.com/orders/{id}"), cancellationToken: attempt),
cancellationToken: cancellationToken);
if (result.TryGetValue(value: out var order))
return order;
// The failure, and everything that led to it, without an exception.
Console.WriteLine(value: $"{result.StopReason}: {result.Attempts}");
return null;
}Key implementation details
- HTTP Classification:
Resilience.HttpusesClassifier.Httpto determine if a failure is worth retrying. It treats 503 responses as transient failures and 429 responses as throttling events, while treating 404 responses as permanent failures. - Backoff and Jitter: By default, the policy makes three attempts with exponential backoff and full jitter (see
Backoff.Default). Backoff introduces a short delay before each retry, and jitter randomizes that delay to prevent multiple clients from retrying at the exact same time. If the server provides aRetry-Afterheader, NResilience respects that value over the backoff curve. - Deadlines: The
Deadlineproperty bounds the total time for the operation, including all retries and backoff delays. This prevents the call from hanging indefinitely. The attempt ceiling stays at its default of 10 seconds, capped by whatever is left of the deadline. See Deadlines for more details. - Retry Budget: A private retry budget is automatically enabled. This caps retries as a fraction of total traffic, preventing the client from becoming a "load generator" that overwhelms a struggling dependency during a broad outage.
- Outcome Reporting:
TryRunAsyncreturns aCallResultinstead of throwing exceptions for expected resilience failures. This provides a clean way to inspect the outcome and the attempt log.
Use a handler for shared clients
If multiple parts of your application share a single HttpClient, it is more efficient to attach the policy to the client using a handler rather than wrapping every individual call site.
The resilience handler clones each request to allow retries, ensures that POST requests are handled according to idempotency rules, and scopes circuit breakers per host.
// One long-lived client. The per-host breakers and budgets live on the handler, and are worth
// nothing to a client that is rebuilt per call.
private static async Task<HttpStatusCode> ReadOrderAsync(CancellationToken cancellationToken)
{
using var client = ResilienceHttp.CreateClient();
using var response = await client.GetAsync(
requestUri: new Uri(uriString: "https://api.example.com/orders/1"), cancellationToken: cancellationToken);
return response.StatusCode;
}In applications using a dependency injection container, you can achieve this by calling AddResilience().
Handle the outcome
Use result.TryGetValue(out var order) to determine if the call succeeded. If the call fails, result.StopReason explains why the operation stopped:
AttemptsExhausted: All retry attempts were used.DeadlineExceeded: The overall time limit was reached.DependencyUnavailable: The circuit breaker is refusing calls.BudgetExhausted: The retry budget was spent.Permanent: The classifier determined that the failure would not change upon retrying.
The result.Attempts property provides a log of every attempt, including the verdict and the delay before that attempt.
For more information
- Classification: Learn how to add your own status codes or exceptions to the classifier.
- Idempotency: Learn when it is safe to retry
POSTrequests. - Why one flat executor: Understand the performance cost of a resilience call.
