Retry Failed Requests in C#
Learn how to retry transient HTTP failures in C# with Microsoft’s current resilience APIs, safe backoff, idempotency rules, diagnostics, and production limits.

Direct answer: For a new .NET HTTP client, register the client with AddHttpClient and add Microsoft’s Microsoft.Extensions.Http.Resilience standard handler. It provides bounded retries with exponential backoff and jitter, a total timeout, rate limiting, and a circuit breaker. Retry transient failures such as HTTP 408, 429, 500 and higher, HttpRequestException, and timeout rejections. Do not blindly retry authentication errors, validation errors, or non-idempotent writes.
A retry count is the number of attempts after the first request. Three retries can therefore execute an operation four times. That distinction affects latency, server load, billing, and duplicate side effects.
1. Install the current resilience package
Microsoft marks Microsoft.Extensions.Http.Polly as deprecated for new work. Use the current resilience package instead:
dotnet add package Microsoft.Extensions.Http.Resilience
The examples below target the modern IHttpClientFactory integration. Check the package version and target framework used by your application because APIs and defaults can change.
2. Add standard retries to an HttpClient
This registration is the normal starting point:
using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddHttpClient<MyApiClient>(client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
client.Timeout = Timeout.InfiniteTimeSpan;
})
.AddStandardResilienceHandler(options =>
{
// The standard handler retries all methods by default.
// Exclude methods that can create or mutate data unless
// the remote API provides idempotency protection.
options.Retry.DisableForUnsafeHttpMethods();
});
var app = builder.Build();
app.Run();
The standard pipeline includes a rate limiter, total timeout, retry strategy, and circuit breaker. Microsoft documents defaults of three retries, exponential backoff, jitter, a two-second delay setting, and a 30-second total timeout. Treat these as library defaults to review, not as a universal policy for every endpoint.
A typed client can then use the factory-managed HttpClient:
public sealed class MyApiClient
{
private readonly HttpClient _http;
public MyApiClient(HttpClient http) => _http = http;
public async Task<string> GetWidgetAsync(
string id,
CancellationToken cancellationToken = default)
{
using var response = await _http.GetAsync(
$"widgets/{Uri.EscapeDataString(id)}",
cancellationToken);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync(cancellationToken);
}
}
With this setup, a transient failure is retried by the resilience handler before GetWidgetAsync receives the final failure. Your application still needs to log the final exception and return an appropriate response to its caller.
3. Decide which requests are safe to retry
| Request or failure | Typical decision | Reason |
|---|---|---|
| GET, HEAD, OPTIONS | Usually retry | These methods are intended to be safe, assuming the endpoint follows HTTP semantics. |
| HTTP 408 | Retry | The server or intermediary timed out the request. |
| HTTP 429 | Retry with server guidance | Respect Retry-After when supplied. |
| HTTP 500–599 | Usually retry | The failure may be transient, although a persistent outage should open the circuit. |
HttpRequestException |
Usually retry | DNS, connection, or transport failures can clear. |
| 401 or 403 | Do not repeat unchanged | Fix credentials, authorization, or token acquisition first. |
| 400 or 422 | Do not repeat unchanged | The payload or parameters need correction. |
| POST/PATCH/PUT/DELETE | Only with idempotency | The first request may have committed even when its response was lost. |
The standard handler retries all HTTP methods unless configured otherwise. DisableForUnsafeHttpMethods() excludes POST, PATCH, PUT, DELETE, and CONNECT. You can also use DisableFor to exclude selected methods.

If a write must be retried, use an idempotency key supported by the API. The server stores the key and returns the original result for duplicate submissions. A client-generated GUID is commonly used, but the key format, retention period, and scope must come from the API contract. Without server-side deduplication, a retry can create two orders, charges, or records.
4. Customize the retry strategy
Use AddResilienceHandler when the standard pipeline does not match your service. The exact option names depend on the package version, so compile against your installed reference documentation.

using Microsoft.Extensions.Http.Resilience;
using Polly;
builder.Services
.AddHttpClient("catalog", client =>
{
client.BaseAddress = new Uri("https://catalog.example.com/");
})
.AddResilienceHandler("catalog-policy", pipeline =>
{
pipeline.AddRetry(new HttpRetryStrategyOptions
{
MaxRetryAttempts = 2,
Delay = TimeSpan.FromMilliseconds(500),
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
ShouldRetryAfterHeader = true
});
});
Keep the total timeout outside your mental retry count. If each attempt can run for 10 seconds and you allow three retries, the caller may wait close to 40 seconds once delays are included unless a total timeout stops the operation earlier. Pass a cancellation token from the request or job so abandoned work stops promptly.
Exponential backoff gives a recovering service time to respond. Jitter randomizes delays so thousands of clients do not retry at the same instant. When a response includes Retry-After, enable the option that uses it and ensure the resulting delay still fits your total latency budget.
5. Understand the circuit breaker
A retry handles a short-lived failure. A circuit breaker handles a sustained one. After enough failures, the breaker temporarily rejects calls instead of sending more traffic to an unhealthy dependency. After its break duration, it permits a trial request. If that succeeds, normal traffic resumes; if it fails, the circuit remains open.
Do not confuse a breaker rejection with a successful fallback. Your code must decide whether to return cached data, enqueue work, show a degraded response, or report an error. Log breaker state changes and include the downstream service name in structured logs.
6. Manage HttpClient lifetime correctly
Retry configuration does not fix poor connection management. Prefer a long-lived client with a suitable PooledConnectionLifetime, or clients created by IHttpClientFactory. Creating and disposing a new HttpClient for every request causes unnecessary connection creation and can exhaust ephemeral ports.
builder.Services.AddHttpClient("dns-aware", client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
})
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(5)
})
.AddStandardResilienceHandler();
Factory-created handlers are pooled. That also means cookie-container state can be shared between requests. If your application requires isolated cookies, configure the handler lifetime and cookie behavior deliberately instead of assuming every client has a private container.
7. Observe retries without hiding failures
Record the logical operation, destination host, HTTP method, attempt number, final status, elapsed time, and whether the circuit was open. Never log authorization headers, cookies, or request bodies that contain secrets.
Useful metrics include:
- Retry count by endpoint and reason.
- Final failure rate after all attempts.
- Latency including backoff delays.
- Circuit-open duration and rejected-call count.
- HTTP 429 frequency and the delay requested by
Retry-After.
A high retry count with a low final failure rate can still indicate unnecessary latency and load. A high final failure rate means retries are not repairing the underlying problem.
8. Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| The request runs four times. | MaxRetryAttempts is three. |
Remember that retries are additional to the initial attempt; lower the count if needed. |
| Two records are created. | An unsafe write was retried after the server committed it. | Disable retries for that method or send an API-supported idempotency key. |
| 429 responses continue. | The client ignores server throttling guidance. | Honor Retry-After, reduce concurrency, and apply a rate limit. |
| Every call fails immediately after an outage. | The circuit is open. | Inspect breaker events and wait for the half-open trial; provide a documented fallback. |
| Requests never finish within the expected time. | Per-attempt timeout and total timeout are being counted separately. | Set one explicit total budget and propagate cancellation tokens. |
| Cookies appear to leak between users. | Pooled factory handlers share cookie state. | Use isolated handlers or avoid shared cookie containers for user-specific sessions. |
AddStandardResilienceHandler is missing. |
The package is absent or an incompatible version is referenced. | Install Microsoft.Extensions.Http.Resilience and verify the target framework and package version. |
| Retries do not happen for a custom exception. | The default predicate does not classify it as transient. | Use AddResilienceHandler and add a narrowly scoped custom predicate. |
9. Performance, reliability, and cost considerations
- Latency: every retry delay is added to the caller’s time. Set a total timeout that matches the user or job deadline.
- Load: retries multiply traffic during an incident. Bound attempts, add jitter, and use a circuit breaker.
- Concurrency: a retry storm can consume thread, socket, and downstream quotas. Combine retries with rate limiting and bounded work queues.
- Payload cost: large uploads may be expensive or slow to repeat. Prefer resumable uploads or an idempotent upload protocol.
- DNS changes: configure connection pooling so long-lived clients eventually refresh DNS according to your deployment needs.
- Testing: exercise timeouts, connection resets, 429 with and without
Retry-After, 500 responses, cancellation, breaker opening, and duplicate-write protection.
Do not add retries to database writes, message publishing, payments, or other non-HTTP operations by analogy. Their transaction and deduplication guarantees must be designed for that system.
10. Or skip the browser setup
If the HTTP request you need is a website screenshot, you can avoid operating a browser, retry policy, and page-cleanup pipeline yourself with ScreenshotNeo. It accepts one GET request and returns PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation for all options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));
Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits are never billed, and response headers identify the page verdict and whether it was billed. An MCP server lets AI agents such as Claude and Cursor call take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
11. FAQ
Should I retry every 5xx response?
No. Most 5xx responses are candidates for a bounded retry, but a persistent outage should trip the circuit breaker. Keep the attempt count and total timeout small enough for your dependency’s recovery behavior.
Is Polly still usable?
Older Polly integrations explain concepts such as wait-and-retry, but Microsoft’s current direction for HttpClient is Microsoft.Extensions.Http.Resilience. Treat Microsoft.Extensions.Http.Polly examples as legacy when starting a new application.
How many retries should production use?
There is no universal number. Start with the documented standard defaults, measure latency and failure recovery, then adjust for the endpoint’s deadline, rate limits, and idempotency guarantees.
Can a retry fix a 401 response?
Repeating the same request normally cannot. Refresh or replace the credential, then issue a new request under the authentication flow defined by the service.
Does a timeout prove the server did not process the request?
No. The server may have committed the operation while the client timed out waiting for the response. This is why unsafe writes require idempotency or deduplication before retries are enabled.


