IHttpClientFactory in ASP.NET Core: Named, Typed, and Basic Clients β Enterprise Decision Guide

Every ASP.NET Core application that calls external services eventually faces the same decision: how should we manage HttpClient instances? The answer, for most teams, is IHttpClientFactory β but the factory itself offers three distinct registration patterns, each with different trade-offs for enterprise codebases. Choosing the wrong one does not just create technical debt; it creates socket exhaustion bugs, DNS staleness issues, and integration test nightmares that surface only under production load.
π‘ Want implementation-ready .NET source code you can adapt fast? Join Coding Droplets on Patreon. π https://www.patreon.com/CodingDroplets
Why HttpClient Management Is an Enterprise-Level Concern
In a single-developer project, creating an HttpClient instance per request feels harmless. In an enterprise system with dozens of services, that instinct causes real outages. The two well-documented failure modes β socket exhaustion from disposing HttpClient too aggressively, and stale DNS from holding onto one instance too long β both stem from the same root: teams treating HttpClient as a utility object rather than a lifetime-managed resource.
IHttpClientFactory was introduced to solve exactly this. It pools HttpMessageHandler instances internally, rotates them on a configurable interval (default two minutes), and exposes a clean registration API. What it does not tell you is which registration pattern fits your team's architecture.
The Three Registration Patterns and What They Signal
Basic Factory Injection
The simplest pattern injects IHttpClientFactory directly into a service and calls CreateClient() at runtime. This gives you a new HttpClient instance backed by a pooled handler. It asks nothing from the DI container about what the client will talk to.
This pattern makes sense when the calling code knows the target base address at runtime, when the destination varies by request context, or when you want to keep configuration logic centralized in the calling class. The trade-off is that you are accepting configuration sprawl β each class that needs a different base address or header policy must manage those concerns itself.
In enterprise terms, Basic Factory injection is a low-governance choice. It works, but it does not enforce naming conventions, header policies, or resilience defaults at registration time.
Named Clients
Named clients let teams pre-register an HttpClient configuration under a string key. At call time, the calling service requests a client by that name and receives an instance pre-configured with the base address, headers, and policies already applied.
This pattern maps well to enterprise architectures where the API catalog is known at startup β where you have an inventory service, an identity provider, a payment gateway β and where different teams own different integrations. Named clients make that catalog explicit in the DI registration layer, which is also where they introduce a risk: string-keyed lookups are refactoring-opaque. A renamed string in one file does not break the build; it breaks at runtime.
Named clients are the right choice when you have a bounded, known set of downstream dependencies, when different client configurations should be co-located in a central registration file, and when your team is comfortable with the operational trade-off of string keys. In practice, most platform teams use named clients for internal shared infrastructure (like a centralized observability endpoint or internal auth service) where the name is treated as a contract.
Typed Clients
Typed clients solve the string-key problem by wrapping a specific HttpClient configuration in a dedicated class. The class takes HttpClient as a constructor parameter, is registered with a scoped lifetime by default, and callers inject the typed service directly β no string magic, no factory calls in business logic.
This is the pattern that most enterprise teams eventually standardize on. It aligns with DDD and clean architecture thinking: each external dependency gets its own abstraction, discoverable via IntelliSense, mockable in tests, and configurable in one place. The downside is that typed clients have a lifetime gotcha: because they are registered as scoped by default, injecting them into singleton services produces a captive dependency. Teams that register typed clients inside singleton-lifetime services will see stale HttpClient instances and intermittent connection failures.
The Typed Client pattern is the right long-term enterprise choice when your downstream dependencies are stable, your service boundaries are well-defined, and your team practices test-driven development.
Lifetime Management: The Constraint That Governs Everything
The most consequential thing about IHttpClientFactory is not the pattern you choose β it is understanding that the factory manages HttpMessageHandler lifetime, not HttpClient lifetime. Every call to CreateClient() or every typed client injection produces a new HttpClient, but that new client reuses an existing, pooled HttpMessageHandler until the rotation interval expires.
This has architectural implications:
For Named Clients: HttpClient instances are transient. Do not cache or store them between requests.
For Typed Clients: The typed class itself is scoped by default. Its HttpClient is effectively per-scope. If you override the lifetime to singleton, you own the DNS staleness risk.
For Basic Factory: Each CreateClient() call is safe because the factory handles handler recycling. But you are deferring all configuration policy away from the DI layer.
Enterprise teams that get this right typically document the lifetime contract explicitly β either in a shared architectural decision record or as an inline registration comment β because the runtime behavior is non-obvious to anyone not deeply familiar with the factory's internals.
Resilience Policy Governance: Where Named and Typed Clients Earn Their Keep
One of the strongest arguments for Named or Typed clients over Basic Factory injection is that they make resilience policy visible at registration time. When teams integrate with Polly for retry, circuit breaker, and timeout policies, those policies attach to the IHttpClientBuilder returned by AddHttpClient. Basic Factory usage forgoes this integration point.
In enterprise systems, resilience policy should be a first-class decision, not an afterthought added by individual developers when a service starts flaking. Named and Typed clients force that policy decision to happen during registration β which means it can be reviewed, tested, and governed as part of your infrastructure layer rather than buried in service method implementations.
Header Propagation and Delegating Handlers
Both Named and Typed clients support custom HttpMessageHandler delegation chains. This is how teams propagate correlation IDs, inject authorization tokens, add telemetry, or enforce mutual TLS β all without touching calling code.
In enterprise architectures, delegating handler chains become the standard pattern for cross-cutting concerns on outbound HTTP. A team that standardizes on Named or Typed clients can enforce that every external call carries a correlation header and an authorization token, configured once, applied everywhere.
Basic Factory injection does not prevent this β you can create handler pipelines manually β but it does not make it easy or consistent. The factory pattern choice, in this sense, is also a governance choice about where your cross-cutting concerns live.
The Decision Framework
When choosing between patterns for a given integration, enterprise architects should evaluate four dimensions:
Stability of the downstream dependency: If the base URL and auth contract are fixed at startup, Named or Typed clients are the right choice. If they vary by request, Basic Factory injection gives you flexibility without unnecessary abstraction.
Team size and autonomy: In large teams, Named clients work well when a platform team owns the registration and service teams consume by name. Typed clients work better in feature-team models where each team owns its own integrations end to end.
Testability requirements: Typed clients win on testability. Mocking a concrete typed service in tests is straightforward. Named clients and Basic Factory injection require more test setup.
Lifetime sensitivity: If the integration will ever be consumed from a singleton service, design the Typed Client registration explicitly around that constraint. A singleton lifetime typed client is valid but requires explicit SetHandlerLifetime configuration to avoid staleness.
Migration Patterns: Moving From Basic to Typed
Most enterprise codebases that have grown organically start with scattered new HttpClient() calls or bare factory injection and eventually need to migrate to Named or Typed clients for governance reasons. The migration is not disruptive β it is additive.
Start by inventorying all downstream HTTP dependencies. Register each as a Named client with the current base address and headers. Then, in a subsequent phase, wrap high-priority clients in Typed classes. This gives teams an observable registration catalog before committing to any structural changes in calling code.
The migration also surfaces something valuable: every external HTTP dependency your system has. Teams that complete this exercise routinely discover undocumented dependencies, orphaned integrations, and inconsistent retry policies that have been running in production without anyone noticing.
Common Governance Anti-Patterns
Injecting IHttpClientFactory and calling CreateClient(""): An empty string is a valid name β it returns the default client β but it makes the dependency implicit. Every team member must know what the default client is configured with.
Using Typed Clients inside Background Services: Background services run in the singleton scope. Typed clients are scoped by default. Injecting them via constructor will appear to work β and then produce subtle bugs as the scoped instance is retained well past its intended lifetime.
Configuring resilience policies per-call: If retry logic lives inside service method implementations rather than in the registration pipeline, it cannot be tested, reviewed, or standardized consistently. This is the pattern that causes retry storms.
Ignoring SetHandlerLifetime: The default handler lifetime is two minutes. For external APIs with aggressive DNS TTLs or frequent endpoint changes, this may be too long. For internal services on stable infrastructure, it may be unnecessarily short. The default is a starting point, not a recommendation.
What Enterprise Standardization Actually Looks Like
Teams that have matured their IHttpClientFactory usage typically settle on a small set of conventions: all external HTTP clients are registered as Typed, all internal shared infrastructure clients are Named with names defined as constants, handler lifetime is set explicitly and documented, and resilience policies are registered at the factory level, not in service methods.
The registration file becomes an architecture artifact β a readable inventory of every external dependency the application holds, along with the policies governing each. New team members can understand the dependency graph without tracing call chains. Code reviewers can spot missing resilience policies at the PR stage. Platform teams can audit and update handler configuration without touching business logic.
That is what good IHttpClientFactory governance looks like at scale. The pattern choice is secondary to the consistency.
FAQ
Q: Can I mix Named and Typed client patterns in the same application?
Yes, and many enterprise applications do. The typical split is: Typed clients for service-owned integrations (each feature team wraps their downstream APIs), and Named clients for platform-owned shared infrastructure (centralized observability, internal auth tokens). The key is to document the convention so both patterns do not proliferate independently.
Q: Is Basic Factory injection an anti-pattern in enterprise systems?
Not categorically β it has legitimate uses when destinations are dynamic or when you are writing infrastructure code that itself manages client configuration. The concern is using it as a default for all HTTP calls in a large codebase, which leads to undiscoverable dependencies and ungovernable resilience policies.
Q: How does the handler rotation interval interact with connection pooling?
The factory's default two-minute handler rotation creates new HttpMessageHandler instances on a timer. Each new handler gets its own connection pool. This means long-lived connections to the same host may be closed and re-established on handler rotation, which is the intentional behavior β it prevents DNS staleness. If your external API is latency-sensitive and connection setup is expensive, consider increasing the rotation interval and testing the DNS staleness risk explicitly.
Q: Do Typed Clients work with Minimal API endpoints?
Yes. Typed clients are registered in the DI container and can be injected into Minimal API endpoint handlers, background services, and anywhere else the container resolves dependencies. The scoped lifetime restriction still applies β injecting a Typed Client into a singleton scope will cause issues.
Q: What is the recommended pattern for calling APIs that require per-request authorization tokens?
Delegating handlers are the standard answer. Register a custom DelegatingHandler that fetches the authorization token and adds it to the outgoing request. Attach this handler to the Named or Typed client at registration time. The handler can itself inject scoped services (like a token cache) if it is registered as scoped β note that handler DI scopes in the factory work differently from request scopes, which is a separate nuance worth understanding before implementing.
Q: How should teams handle Typed Clients in integration tests?
The recommended approach is to use WebApplicationFactory<TProgram> and override the typed client registration in the test setup to point at a mock or stub HTTP server β typically backed by a library like WireMock or a simple HttpMessageHandler mock. This keeps integration tests realistic without hitting real external services.
Q: Should handler lifetime be configured globally or per-client?
Per-client. Each downstream dependency has different DNS and connection characteristics. A payment gateway client may warrant a shorter rotation interval than an internal metadata service on a stable private network. Setting a global handler lifetime trades convenience for correctness in heterogeneous dependency graphs.






