For a small console app or service without dependency injection, reuse a configured HttpClient with a SocketsHttpHandler and an appropriate PooledConnectionLifetime. For a dependency-injection-based application, use IHttpClientFactory; choose a named client for shared configuration or a typed client to keep one API’s endpoints and data mapping behind a service class. Add resilience, streaming, and connection limits only when the API contract and your traffic call for them.
Choose a client pattern that fits your application
HttpClient is .NET’s primary HTTP abstraction. Microsoft’s guidance supports two sound lifetime strategies: a long-lived client with PooledConnectionLifetime, or short-lived clients created by IHttpClientFactory. Each HttpClient has a connection pool; creating and disposing one for every request can cause unnecessary connection creation and port exhaustion.
| Approach | Client lifetime and DNS | DI and API configuration | Handlers, testing, and cautions |
|---|---|---|---|
Reusable HttpClient |
Keep the client alive and set PooledConnectionLifetime on its handler so connections are periodically replaced; choose the interval for the environment’s DNS behavior. |
Simple for a console app or small service without DI. Configure one base address and shared defaults. | Handler composition is available, but configuration and test seams are more manual. A shared cookie container may not suit strict cookie isolation. |
| Named factory client | Create a short-lived client with CreateClient; the factory pools handlers. Do not cache the returned client indefinitely. |
Fits DI applications and multiple APIs with different base addresses, headers, credentials, or handlers. | Centralized configuration and handler composition make it practical to test through an injected boundary. Pooled handlers can share cookie state and lose cookies when handlers recycle. |
| Typed factory client | The factory-created typed client is intended to be short-lived. Do not inject it into a singleton service. | Fits DI applications that benefit from a dedicated class for one API’s endpoints and DTO mapping. | Encapsulates the API behind a service boundary and can be tested through its HTTP handler; cookie considerations are the same as other factory clients. |
Reuse one client in a small app without DI
Configure the handler and client once, then reuse the client across requests. PooledConnectionLifetime is an operational choice based on expected DNS changes, not a universal five-minute rule.
using System.Net.Http.Json;
var handler = new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(5)
};
using var client = new HttpClient(handler)
{
BaseAddress = new Uri("https://api.example.com/")
};
var item = await client.GetFromJsonAsync<Item>("items/42");
The five-minute interval shown is an example configuration, not a Microsoft-mandated value. Set it to match how quickly DNS changes need to be reflected in your environment.
Free tools Windows power users keep installed
One-click scans. No signup required.
#1 Best Overall
Use a named client for shared endpoint configuration
Register a logical client when different remote APIs need different defaults. Inject IHttpClientFactory where a request is made and ask it for the named client. The factory returns a new HttpClient object while pooling handlers underneath, so disposing a returned client is safe.
builder.Services.AddHttpClient("catalog", client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
client.DefaultRequestHeaders.Add("Accept", "application/json");
});
public sealed class CatalogGateway(IHttpClientFactory factory)
{
public Task<Item?> GetAsync(int id, CancellationToken ct) =>
factory.CreateClient("catalog")
.GetFromJsonAsync<Item>($"items/{id}", ct);
}
Creating a client for each operation is compatible with factory-managed handler pooling; retaining that client for the lifetime of a service defeats the intended short-lived-client pattern.
Use a typed client to keep API details out of application code
A typed client is a service-agent-style wrapper for one API. Put endpoint paths, DTO mapping, and API-specific error translation in it instead of scattering them through business logic.
Rank #2
builder.Services.AddHttpClient<CatalogClient>(client =>
client.BaseAddress = new Uri("https://api.example.com/"));
public sealed class CatalogClient(HttpClient http)
{
public Task<Item?> GetAsync(int id, CancellationToken ct) =>
http.GetFromJsonAsync<Item>($"items/{id}", ct);
}
Typed clients fit naturally into DI, but keep them short-lived: do not inject one into a singleton service. Singleton code that needs an API should instead obtain a short-lived client through an appropriate factory-based design.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →Deserialize JSON, but inspect responses when the contract requires it
System.Net.Http.Json provides concise helpers built on System.Text.Json. For ordinary JSON endpoints, GetFromJsonAsync<T> and PostAsJsonAsync avoid boilerplate.
Use SendAsync when the caller needs to inspect status, headers, or an error body before deciding what to do. The response-aware path also makes the distinction between HTTP success and a valid, usable domain result explicit.
using var response = await client.GetAsync("items/42", ct);
if (!response.IsSuccessStatusCode)
{
var detail = await response.Content.ReadAsStringAsync(ct);
throw new HttpRequestException(
$"API returned {(int)response.StatusCode}: {detail}");
}
var item = await response.Content.ReadFromJsonAsync<Item>(
cancellationToken: ct);
A production gateway should handle the failure classes the API can actually produce:
- Cancellation: pass the caller’s
CancellationTokenthrough request and content-reading calls so abandoned work can stop. - Timeouts and transport exceptions: distinguish failures to complete the HTTP exchange from an HTTP response that reports an error.
- Non-success status codes: inspect the status and any contractually meaningful error payload before translating it to an application-level result or exception.
- Malformed JSON: handle deserialization failure separately from an unsuccessful HTTP status.
- Domain-level errors: an HTTP-success response can still represent a failure in the API’s own response model; interpret that model according to its contract.
Do not treat an error body as safe to log by default; it may contain sensitive data.
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Add resilience without retrying blindly
For DI-based clients, Microsoft.Extensions.Http.Resilience adds standard or custom resilience pipelines to AddHttpClient. The package relies on Microsoft.Extensions.Resilience and Polly. Microsoft advises using one resilience handler rather than stacking handlers, unless a deliberately combined custom handler is required.
builder.Services.AddHttpClient<CatalogClient>(client =>
client.BaseAddress = new Uri("https://api.example.com/"))
.AddStandardResilienceHandler();
A standard handler is a starting point, not a promise that every retry is safe for every endpoint. Before enabling retries, decide whether the operation is safe to repeat and check the provider’s rate limits and guidance. Backoff and cancellation matter: immediately repeating work can increase load during an outage, and retrying a non-idempotent operation can produce duplicate effects. Review retry, timeout, circuit-breaker, or hedging settings against those constraints rather than copying defaults without evaluation.
The package API is version-sensitive. Verify that Microsoft.Extensions.Http.Resilience and AddStandardResilienceHandler are available and appropriate for the target .NET and package versions.
Stream large responses and manage concurrency deliberately
By default, many convenience methods buffer response content. Microsoft’s .NET networking guidance says applications downloading large amounts of data—50 megabytes or more—should stream rather than use default buffering. Request ResponseHeadersRead, then process the response stream incrementally:
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Scan for outdated or missing drivers - takes under a minute3Repair Windows errors before they cause bigger problemsBest Value
using var response = await client.GetAsync(
"exports/large-file",
HttpCompletionOption.ResponseHeadersRead,
ct);
response.EnsureSuccessStatusCode();
await using var input = await response.Content.ReadAsStreamAsync(ct);
await using var output = File.Create("export.bin");
await input.CopyToAsync(output, ct);
For transformations or very large files, read and process bounded chunks instead of copying the entire response into an in-memory buffer. The example writes to disk to keep the full payload out of application memory.
If many HTTP/1.1 requests to one server run concurrently, consider a reasonable MaxConnectionsPerServer on the handler. HTTP/2 multiplexes requests differently and may be appropriate when supported by the endpoint. Choose based on protocol availability and the application’s concurrency profile, not as a universal tuning setting.
Account for cookies, authentication, and handler scope
Factory-managed handlers can share CookieContainer state, and cookies may be lost when pooled handlers recycle. If the application requires strict per-user cookie isolation or persistent cookie behavior, evaluate whether a factory-managed pooled-handler lifetime is appropriate before adopting it.
Delegating handlers can centralize cross-cutting HTTP behavior such as authentication, correlation IDs, logging, and redaction. Keep their scopes and shared state intentional. Never log bearer tokens or sensitive response bodies. The right credential and cookie design depends on the API’s authentication contract.
Make the API boundary testable and maintainable
A typed client or gateway creates a natural place to keep endpoint paths, DTOs, JSON options, and API-specific errors. In tests, inject or provide an HttpMessageHandler that returns deterministic HttpResponseMessage instances; this lets tests verify request construction and response handling without making live network calls.
Quick Recap
- Test success responses, non-success statuses, malformed JSON, and cancellation behavior that matters to the caller.
- Keep tests aligned with the API contract; a handler-based test proves your client’s behavior for the simulated response, not that the remote service currently honors the contract.
- Use contract or integration tests separately when you need evidence about compatibility with a live service.
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

