The 12-Point IHttpClientFactory Checklist for ASP.NET Core .NET Teams

Most teams start using IHttpClientFactory because someone told them to stop newing up HttpClient. That swap takes five minutes. What they miss is the twelve other decisions that determine whether IHttpClientFactory actually holds up in production โ or quietly causes socket exhaustion, hanging requests, and silent retry storms at 3 AM.
This checklist captures what a correctly configured IHttpClientFactory setup looks like for a production ASP.NET Core API. Work through it once, and you will know exactly where your setup stands. For working source code that wires together each of these controls inside a production-grade API, Patreon has the full implementation with every pattern connected and tested.
Getting resilience right inside IHttpClientFactory is one chapter of a larger story. Understanding how retry, circuit breaker, and timeout policies compose with each other โ alongside rate limiting and the full outbound request pipeline โ is something Chapter 10 of the Zero to Production course covers in context, inside a complete API codebase rather than in isolation.
What Does a Correctly Configured IHttpClientFactory Look Like?
A correctly configured IHttpClientFactory setup in ASP.NET Core registers all HTTP clients at startup, applies explicit timeouts and resilience policies to each client, respects handler lifetime boundaries, and avoids injecting typed clients into singleton services. Every client should have a named configuration โ no anonymous registrations that leave resilience to chance in production.
The 12-Point Checklist
1. Never Instantiate HttpClient Directly
If any part of your codebase calls new HttpClient() without a corresponding PooledConnectionLifetime on the underlying SocketsHttpHandler, you are risking socket exhaustion under load. The fix is not complicated โ register every client through IHttpClientFactory and let the framework manage handler lifetimes. Search your codebase for new HttpClient and treat every result as a defect worth addressing before the next production incident. Two patterns are acceptable: factory-managed clients, and manually managed SocketsHttpHandler with an explicit PooledConnectionLifetime. Everything else is risk.
2. Choose One Registration Pattern and Apply It Consistently
IHttpClientFactory supports three registration patterns: basic factory injection, named clients, and typed clients. Each has a legitimate use case, but mixing all three across the same codebase creates configuration drift โ timeout policies end up living in different places, resilience settings are applied inconsistently, and onboarding takes longer than it should. Pick the pattern that fits your architecture and standardise on it. For most enterprise APIs, typed clients are the right default โ they move HTTP configuration off the call site and into a dedicated class that is independently testable and configurable at registration time.
3. Set an Explicit Timeout on Every Client
HttpClient has a default timeout of 100 seconds. That number is almost never right for a production API calling external services. An unresponsive downstream dependency should not be allowed to hold a thread for nearly two minutes. Set explicit timeouts that reflect the expected response characteristics of the service you are calling โ not just a global fallback chosen to avoid the question. Short per-try timeouts (typically 3โ10 seconds for synchronous APIs) combined with a retry budget give you meaningful control that a single long timeout cannot. If you cannot answer what the acceptable response time is for a given external service, that conversation needs to happen before the timeout is set.
4. Attach a Resilience Pipeline โ Do Not Rely on the Default No-Policy State
A raw IHttpClientFactory registration with no resilience policy is a configuration waiting to cause problems. For any external dependency that matters to your API's reliability, attach a resilience pipeline that covers at minimum: retry with exponential backoff, a circuit breaker, and a timeout. In .NET 8 and later, AddStandardResilienceHandler() from Microsoft.Extensions.Http.Resilience gives you a well-considered baseline in one call. Adjust its parameters to match your workload's tolerance โ but never ship to production with unprotected external HTTP calls on critical paths.
5. Never Inject Typed Clients Into Singleton Services
This is among the most common misconfiguration mistakes teams make after switching to typed clients. A typed client wraps an HttpClient instance, and those instances are expected to be short-lived โ created, used, and released on the factory's rotation schedule (default: two minutes). Injecting a typed client into a singleton freezes the underlying HttpMessageHandler indefinitely, preventing the rotation that guards against DNS staleness. The recommended pattern when you genuinely need HTTP access inside a singleton is to inject IHttpClientFactory directly and call CreateClient() within the method that makes the request, not in the constructor.
6. Verify Handler Lifetime Is Explicitly Configured
The default handler lifetime is two minutes โ a deliberate balance between DNS responsiveness and connection reuse overhead. For most workloads this default holds. But services with aggressive DNS TTLs or IP rotation may benefit from a shorter lifetime, while infrequently accessed services might warrant a longer one to reduce unnecessary handler churn. Whatever value applies to your workload, set it explicitly so the choice is visible in the codebase. Undocumented reliance on framework defaults is a common source of confusion when diagnosing production incidents.
7. Align Per-Try Timeout With Retry Count Before Deployment
If you configure a per-try timeout of five seconds and a retry count of three, your worst-case latency before the circuit breaker acts is at least fifteen seconds โ longer once you factor in exponential backoff delays. Run the arithmetic against the latency budget of the endpoint before setting these values. For user-facing requests, the acceptable total latency constrains retry depth. For background processing, there is more headroom. Miscalibrated timeout and retry combinations are a common source of cascading slowdowns in distributed systems, particularly under partial outages where the downstream service responds slowly rather than refusing connections.
8. Configure Both Inner and Outer Timeout Layers
When using AddStandardResilienceHandler(), the resilience pipeline has two timeout layers: a per-attempt inner timeout and a total request outer timeout. The outer timeout must be greater than the maximum possible time for all retry attempts to complete, including backoff delays. If it is not, the outer timeout fires before all retries are exhausted, producing an error that looks like a timeout rather than a retry failure โ and silently reducing the resilience you intended to configure. Always review inner and outer timeout values together, not as independent settings.
9. Centralise Base Addresses and Default Headers at Registration Time
Call site code should not be constructing absolute URLs from base addresses, appending authentication tokens, or setting Accept headers on every outgoing request. All of that belongs in the AddHttpClient registration block at startup. Centralising at registration makes configuration auditable in one place, removes duplication from service methods, and ensures that a change to how you authenticate with an external service propagates automatically without hunting through method bodies. If you encounter client.DefaultRequestHeaders.Add(...) scattered across service classes, move it to registration โ that is where it belongs.
10. Use a DelegatingHandler for Cross-Cutting Concerns
Authentication token refresh, correlation ID propagation, outbound request logging, and retry telemetry all belong in DelegatingHandler implementations โ not scattered across the services that use the HTTP client. A delegating handler sits in the pipeline between your code and the outgoing request, providing a clean, independently testable interception point. Register handlers via AddHttpMessageHandler<T>() on the client builder. This also makes it straightforward to compose, reorder, or remove cross-cutting concerns at registration time without modifying any of the service classes that consume the client.
11. Apply Resilience Policies Per Client, Not Globally
A shared global Polly policy applied to every HTTP client in the application appears to be a productivity win. In practice, it means your health check client, your payment gateway client, and your internal telemetry client all share the same retry budget and circuit breaker state. When the telemetry endpoint experiences a blip, the shared circuit opens and begins rejecting calls to the payment gateway too. Resilience policies should be scoped to individual named or typed clients, sized to the risk profile and SLA tolerance of the specific downstream dependency each client is responsible for.
12. Test Resilience Behaviour With MockHttp or WireMock.NET
Mocking IHttpClientFactory in unit tests is technically achievable. It is also a trap โ a mocked factory tells you nothing about whether your retry policy fires on a 503 response, whether your timeout throws the expected exception type, or whether your DelegatingHandler attaches the correct correlation header. Use MockHttp for lightweight handler substitution in unit tests, where you need to control responses without spinning up a server. Use WireMock.NET for integration tests where you need real HTTP behaviour with simulated upstream response scenarios. The investment pays for itself the first time a resilience misconfiguration reaches production and your test suite returned green.
Quick Reference
| Check | What to Verify |
|---|---|
| No direct instantiation | No new HttpClient() without managed handler lifetime |
| Consistent pattern | One registration pattern used uniformly across the codebase |
| Explicit timeouts | Every client has a non-default timeout matching its service's SLA |
| Resilience pipeline | Retry + circuit breaker + timeout on all critical clients |
| No singleton injection | Typed clients never injected into singleton-lifetime services |
| Handler lifetime set | Explicitly configured, not relying on framework defaults |
| Timeout and retry aligned | Outer timeout exceeds all retry attempts including backoff |
| Dual timeout layers | Per-attempt and total timeouts both reviewed together |
| Centralised registration | Base addresses and default headers set at startup, not call site |
| DelegatingHandler pipeline | Cross-cutting concerns in handlers, not service methods |
| Per-client policies | Resilience policies scoped individually, not shared globally |
| Realistic test coverage | Tests use MockHttp or WireMock.NET, not factory mocks |
FAQ
Why does IHttpClientFactory exist if I can just reuse a static HttpClient?
A static HttpClient solves socket exhaustion but introduces DNS staleness โ if the IP address for a host changes after your process starts, a long-lived static client keeps connecting to the original IP until the process restarts. IHttpClientFactory solves both problems simultaneously by pooling HttpMessageHandler instances for connection reuse while rotating them on a configurable schedule to pick up DNS changes. It also integrates natively with the .NET dependency injection and logging infrastructure, which a globally shared static instance cannot.
What is the default handler lifetime in IHttpClientFactory and when should I adjust it?
The default handler lifetime is two minutes. This reflects a deliberate trade-off between connection reuse (longer lifetimes are better) and DNS responsiveness (shorter lifetimes are better). Adjust it shorter if the downstream service relies on DNS-based load balancing with low TTLs. Adjust it longer if the service is called rarely and handler churn creates observable overhead. Whatever you choose, configure it explicitly via SetHandlerLifetime() and document your reason โ undocumented lifetime values are reliably confusing during incident post-mortems.
Is it safe to inject IHttpClientFactory into a singleton service?
Yes โ injecting IHttpClientFactory itself into a singleton is safe and is the explicit recommendation for singleton-lifetime services that need outbound HTTP access. Create the client inside the method that makes the request by calling factory.CreateClient("client-name"), not in the constructor. What is unsafe is injecting a typed client into a singleton, because a typed client wraps a managed HttpClient that is designed to be short-lived and rotated by the factory.
Does AddStandardResilienceHandler replace manual Polly configuration?
AddStandardResilienceHandler() provides a production-ready baseline with sensible defaults for retry, circuit breaker, and timeout behaviour. For most workloads it is the right starting point, and its parameters can be customised via the Configure overload without abandoning the standard structure. Manual Polly pipeline configuration remains appropriate when you need fine-grained control that the standard handler's model does not accommodate โ for example, retry strategies conditioned on response body content, or backoff formulae specific to a particular upstream's rate limiting behaviour.
How do I simulate retry and circuit breaker behaviour in integration tests?
WireMock.NET is the most reliable approach. Spin up a WireMock server in your test setup and configure it to return specific status codes (503, 429, connection timeout simulations) for predetermined request patterns. Your real IHttpClientFactory-registered client makes actual HTTP calls to the WireMock base URL, and the entire pipeline โ retry policy, timeout, DelegatingHandler chain, error mapping โ gets exercised in a single test with no factory mocks required. This catches configuration mistakes that unit tests with mocked factories cannot see.
Can IHttpClientFactory be used in .NET Worker Services and console apps?
Yes. IHttpClientFactory is part of Microsoft.Extensions.Http and is available in any .NET application that uses the generic host (IHost). Worker Services, console applications, and Azure Functions all support the same AddHttpClient() registration API as ASP.NET Core. The configuration patterns โ named clients, typed clients, handler lifetime, resilience pipelines โ behave identically regardless of the host type. The only requirement is that the generic host is configured, which is standard practice for any .NET background service built against the Microsoft.Extensions infrastructure.
These twelve controls address the most common gaps between an IHttpClientFactory setup that runs in development and one that holds up in production. The IHttpClientFactory Enterprise Decision Guide covers the upstream question of which client pattern to adopt โ this checklist assumes you have made that choice and want to verify the configuration surrounding it. For the full picture of how outbound resilience sits alongside rate limiting and request timeouts in a production API, the ASP.NET Core Request Timeout Strategy article covers the intersection of those concerns in detail.




